diff --git a/CHANGELOG.md b/CHANGELOG.md index ae8bc1a37..dda392f0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ The `Unreleased` section name is replaced by the expected version of next releas ### Changed - `SqlStreamStore`.*: Target `SqlStreamStore` v `1.2.0-beta.8` -- Target `FsCodec` v `2.0.0-rc1` +- Target `FsCodec` v `2.0.0-rc2` - Target `Microsoft.SourceLink.GitHub`, `Microsoft.NETFramework.ReferenceAssemblies` v `1.0.0` - Samples etc target `Argu` v `6.0.0` - `eqx dump`'s `-J` switch now turns off JSON rendering @@ -29,6 +29,7 @@ The `Unreleased` section name is replaced by the expected version of next releas ### Removed - `Accumulator` [#184](https://github.com/jet/equinox/pull/184) +- `Target` (now uses `FsCodec.StreamName`) [#189](https://github.com/jet/equinox/pull/189) ### Fixed diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 7b50abebf..de30e1229 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -132,7 +132,7 @@ The following example is a minimal version of [the Favorites model](samples/Stor ```fsharp (* Event stream naming + schemas *) -let (|ForClientId|) (id: ClientId) = Equinox.AggregateId("Favorites", ClientId.toStringN id) +let (|ForClientId|) (id: ClientId) = FsCodec.StreamName.create "Favorites" (ClientId.toString id) type Item = { id: int; name: string; added: DateTimeOffset } type Event = @@ -372,7 +372,7 @@ See [the TodoBackend.com sample](README.md#TodoBackend) for reference info regar #### `Event`s ```fsharp -let (|ForClientId|) (id : string) = Equinox.AggregateId("Todos", id) +let (|ForClientId|) (id : string) = FsCodec.StreamName.create "Todos" id type Todo = { id: int; order: int; title: string; completed: bool } type Event = diff --git a/Equinox.sln b/Equinox.sln index 2dc517ded..ed57c3c0a 100644 --- a/Equinox.sln +++ b/Equinox.sln @@ -16,7 +16,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".project", ".project", "{7E Directory.Build.props = Directory.Build.props Directory.Build.targets = Directory.Build.targets DOCUMENTATION.md = DOCUMENTATION.md - global.json = global.json LICENSE = LICENSE README.md = README.md SECURITY.md = SECURITY.md diff --git a/README.md b/README.md index cb7392378..8b13e127f 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ The components within this repository are delivered as multi-targeted Nuget pack ### Core library -- `Equinox` [![NuGet](https://img.shields.io/nuget/v/Equinox.svg)](https://www.nuget.org/packages/Equinox/): Store-agnostic decision flow runner that manages the optimistic concurrency protocol. ([depends](https://www.fuget.org/packages/Equinox) on `Serilog` (but no specific Serilog sinks, i.e. you configure to emit to `NLog` etc)) +- `Equinox` [![NuGet](https://img.shields.io/nuget/v/Equinox.svg)](https://www.nuget.org/packages/Equinox/): Store-agnostic decision flow runner that manages the optimistic concurrency protocol. ([depends](https://www.fuget.org/packages/Equinox) on `FsCodec` (for the `StreamName` type-contract), `Serilog` (but no specific Serilog sinks, i.e. you configure to emit to `NLog` etc)) ### Serialization support diff --git a/samples/Store/Domain.Tests/Infrastructure.fs b/samples/Store/Domain.Tests/Infrastructure.fs index 3d8054ba8..9d25a7b11 100644 --- a/samples/Store/Domain.Tests/Infrastructure.fs +++ b/samples/Store/Domain.Tests/Infrastructure.fs @@ -32,7 +32,7 @@ module IdTypes = let x = Guid.NewGuid() in let xs, xn = x.ToString(), x.ToString "N" let (x1 : CartId, x2 : CartId) = %x, %x test <@ x1 = x2 - && xn = CartId.toStringN x2 + && xn = CartId.toString x2 && string x1 = xs @> [] @@ -40,7 +40,7 @@ module IdTypes = let x = Guid.NewGuid() in let xs, xn = x.ToString(), x.ToString "N" let (x1 : ClientId, x2 : ClientId) = %x, %x test <@ x1 = x2 - && xn = ClientId.toStringN x2 + && xn = ClientId.toString x2 && string x1 = xs @> [] diff --git a/samples/Store/Domain/Cart.fs b/samples/Store/Domain/Cart.fs index f464e4a59..348515519 100644 --- a/samples/Store/Domain/Cart.fs +++ b/samples/Store/Domain/Cart.fs @@ -2,6 +2,9 @@ // NOTE - these types and the union case names reflect the actual storage formats and hence need to be versioned with care module Events = + + let (|ForCartId|) (id: CartId) = FsCodec.StreamName.create "Cart" (CartId.toString id) + type ContextInfo = { time: System.DateTime; requestId: RequestId } type ItemInfo = { context: ContextInfo; item: ItemInfo } @@ -22,7 +25,6 @@ module Events = | ItemWaiveReturnsChanged of ItemWaiveReturnsInfo interface TypeShape.UnionContract.IUnionContract let codec = FsCodec.NewtonsoftJson.Codec.Create() - let (|ForCartId|) (id: CartId) = Equinox.AggregateId ("Cart", CartId.toStringN id) module Fold = type ItemInfo = { skuId: SkuId; quantity: int; returnsWaived: bool } diff --git a/samples/Store/Domain/ContactPreferences.fs b/samples/Store/Domain/ContactPreferences.fs index e76e5d2a6..263efebef 100644 --- a/samples/Store/Domain/ContactPreferences.fs +++ b/samples/Store/Domain/ContactPreferences.fs @@ -4,6 +4,9 @@ type Id = Id of email: string // NOTE - these types and the union case names reflect the actual storage formats and hence need to be versioned with care module Events = + + let (|ForClientId|) (email: string) = FsCodec.StreamName.create "ContactPreferences" email // TODO hash >> base64 + type Preferences = { manyPromotions : bool; littlePromotions : bool; productReview : bool; quickSurveys : bool } type Value = { email : string; preferences : Preferences } @@ -11,9 +14,9 @@ module Events = | []Updated of Value interface TypeShape.UnionContract.IUnionContract let codec = FsCodec.NewtonsoftJson.Codec.Create() - let (|ForClientId|) (email: string) = Equinox.AggregateId ("ContactPreferences", email) // TODO hash >> base64 module Fold = + type State = Events.Preferences let initial : State = { manyPromotions = false; littlePromotions = false; productReview = false; quickSurveys = false } diff --git a/samples/Store/Domain/Domain.fsproj b/samples/Store/Domain/Domain.fsproj index 557b6e470..d6364b12e 100644 --- a/samples/Store/Domain/Domain.fsproj +++ b/samples/Store/Domain/Domain.fsproj @@ -21,7 +21,7 @@ - + diff --git a/samples/Store/Domain/Favorites.fs b/samples/Store/Domain/Favorites.fs index 9f79b5b35..d9e0bef13 100644 --- a/samples/Store/Domain/Favorites.fs +++ b/samples/Store/Domain/Favorites.fs @@ -2,6 +2,9 @@ // NOTE - these types and the union case names reflect the actual storage formats and hence need to be versioned with care module Events = + + let (|ForClientId|) (id: ClientId) = FsCodec.StreamName.create "Favorites" (ClientId.toString id) + type Favorited = { date: System.DateTimeOffset; skuId: SkuId } type Unfavorited = { skuId: SkuId } type Snapshotted = { net: Favorited[] } @@ -12,9 +15,9 @@ module Events = | Unfavorited of Unfavorited interface TypeShape.UnionContract.IUnionContract let codec = FsCodec.NewtonsoftJson.Codec.Create() - let (|ForClientId|) (id: ClientId) = Equinox.AggregateId("Favorites", ClientId.toStringN id) module Fold = + type State = Events.Favorited [] type private InternalState(input: State) = diff --git a/samples/Store/Domain/Infrastructure.fs b/samples/Store/Domain/Infrastructure.fs index 52bc6902e..31a734962 100644 --- a/samples/Store/Domain/Infrastructure.fs +++ b/samples/Store/Domain/Infrastructure.fs @@ -72,14 +72,14 @@ module RequestId = /// CartId strongly typed id; represented internally as a Guid; not used for storage so rendering is not significant type CartId = Guid and [] cartId -module CartId = let toStringN (value : CartId) : string = Guid.toStringN %value +module CartId = let toString (value : CartId) : string = Guid.toStringN %value /// ClientId strongly typed id; represented internally as a Guid; not used for storage so rendering is not significant type ClientId = Guid and [] clientId -module ClientId = let toStringN (value : ClientId) : string = Guid.toStringN %value +module ClientId = let toString (value : ClientId) : string = Guid.toStringN %value /// InventoryItemId strongly typed id type InventoryItemId = Guid and [] inventoryItemId -module InventoryItemId = let toStringN (value : InventoryItemId) : string = Guid.toStringN %value \ No newline at end of file +module InventoryItemId = let toString (value : InventoryItemId) : string = Guid.toStringN %value \ No newline at end of file diff --git a/samples/Store/Domain/InventoryItem.fs b/samples/Store/Domain/InventoryItem.fs index 2f0797a5e..c2cf01dc0 100644 --- a/samples/Store/Domain/InventoryItem.fs +++ b/samples/Store/Domain/InventoryItem.fs @@ -5,6 +5,9 @@ open System // NOTE - these types and the union case names reflect the actual storage formats and hence need to be versioned with care module Events = + + let (|ForInventoryItemId|) (id : InventoryItemId) = FsCodec.StreamName.create "InventoryItem" (InventoryItemId.toString id) + type Event = | Created of name: string | Deactivated @@ -12,7 +15,6 @@ module Events = | Removed of count: int | CheckedIn of count: int interface TypeShape.UnionContract.IUnionContract - let (|ForInventoryItemId|) (id : InventoryItemId) = Equinox.AggregateId ("InventoryItem", InventoryItemId.toStringN id) module Fold = type State = { active : bool; name: string; quantity: int } diff --git a/samples/Store/Domain/SavedForLater.fs b/samples/Store/Domain/SavedForLater.fs index ec3fc9942..994ccc1f0 100644 --- a/samples/Store/Domain/SavedForLater.fs +++ b/samples/Store/Domain/SavedForLater.fs @@ -5,6 +5,9 @@ open System.Collections.Generic // NOTE - these types and the union case names reflect the actual storage formats and hence need to be versioned with care module Events = + + let (|ForClientId|) (id: ClientId) = FsCodec.StreamName.create "SavedForLater" (ClientId.toString id) + type Item = { skuId : SkuId; dateSaved : DateTimeOffset } type Added = { skus : SkuId []; dateSaved : DateTimeOffset } @@ -27,7 +30,6 @@ module Events = | Added of Added interface TypeShape.UnionContract.IUnionContract let codec = FsCodec.NewtonsoftJson.Codec.Create() - let (|ForClientId|) (id: ClientId) = Equinox.AggregateId("SavedForLater", ClientId.toStringN id) module Fold = open Events diff --git a/samples/TodoBackend/Todo.fs b/samples/TodoBackend/Todo.fs index 27c99d1eb..0349f1ada 100644 --- a/samples/TodoBackend/Todo.fs +++ b/samples/TodoBackend/Todo.fs @@ -3,8 +3,12 @@ open Domain // NOTE - these types and the union case names reflect the actual storage formats and hence need to be versioned with care -[] module Events = + + // The TodoBackend spec does not dictate having multiple lists, tenants or clients + // Here, we implement such a discriminator in order to allow each virtual client to maintain independent state + let (|ForClientId|) (id : ClientId) = FsCodec.StreamName.create "Todos" (ClientId.toString id) + type Todo = { id: int; order: int; title: string; completed: bool } type Deleted = { id: int } type Snapshotted = { items: Todo[] } @@ -16,37 +20,34 @@ module Events = | Snapshotted of Snapshotted interface TypeShape.UnionContract.IUnionContract let codec = FsCodec.NewtonsoftJson.Codec.Create() - // The TodoBackend spec does not dictate having multiple lists, tenants or clients - // Here, we implement such a discriminator in order to allow each virtual client to maintain independent state - let (|ForClientId|) (id : ClientId) = Equinox.AggregateId("Todos", ClientId.toStringN id) module Fold = - type State = { items : Todo list; nextId : int } + type State = { items : Events.Todo list; nextId : int } let initial = { items = []; nextId = 0 } let evolve s e = match e with - | Added item -> { s with items = item :: s.items; nextId = s.nextId + 1 } - | Updated value -> { s with items = s.items |> List.map (function { id = id } when id = value.id -> value | item -> item) } - | Deleted { id=id } -> { s with items = s.items |> List.filter (fun x -> x.id <> id) } - | Cleared -> { s with items = [] } - | Snapshotted { items = items } -> { s with items = List.ofArray items } + | Events.Added item -> { s with items = item :: s.items; nextId = s.nextId + 1 } + | Events.Updated value -> { s with items = s.items |> List.map (function { id = id } when id = value.id -> value | item -> item) } + | Events.Deleted { id=id } -> { s with items = s.items |> List.filter (fun x -> x.id <> id) } + | Events.Cleared -> { s with items = [] } + | Events.Snapshotted { items = items } -> { s with items = List.ofArray items } let fold : State -> Events.Event seq -> State = Seq.fold evolve let isOrigin = function Events.Cleared | Events.Snapshotted _ -> true | _ -> false - let snapshot state = Snapshotted { items = Array.ofList state.items } + let snapshot state = Events.Snapshotted { items = Array.ofList state.items } -type Command = Add of Todo | Update of Todo | Delete of id: int | Clear +type Command = Add of Events.Todo | Update of Events.Todo | Delete of id: int | Clear module Commands = let interpret c (state : Fold.State) = match c with - | Add value -> [Added { value with id = state.nextId }] + | Add value -> [Events.Added { value with id = state.nextId }] | Update value -> match state.items |> List.tryFind (function { id = id } -> id = value.id) with - | Some current when current <> value -> [Updated value] + | Some current when current <> value -> [Events.Updated value] | _ -> [] - | Delete id -> if state.items |> List.exists (fun x -> x.id = id) then [Deleted {id=id}] else [] - | Clear -> if state.items |> List.isEmpty then [] else [Cleared] + | Delete id -> if state.items |> List.exists (fun x -> x.id = id) then [Events.Deleted {id=id}] else [] + | Clear -> if state.items |> List.isEmpty then [] else [Events.Cleared] type Service(log, resolve, ?maxAttempts) = @@ -65,7 +66,7 @@ type Service(log, resolve, ?maxAttempts) = let state' = Fold.fold state events state'.items,events) - member __.List(clientId) : Async = + member __.List(clientId) : Async = query clientId (fun s -> s.items |> Seq.ofList) member __.TryGet(clientId, id) = @@ -74,10 +75,10 @@ type Service(log, resolve, ?maxAttempts) = member __.Execute(clientId, command) : Async = execute clientId command - member __.Create(clientId, template: Todo) : Async = async { + member __.Create(clientId, template: Events.Todo) : Async = async { let! state' = handle clientId (Command.Add template) return List.head state' } - member __.Patch(clientId, item: Todo) : Async = async { + member __.Patch(clientId, item: Events.Todo) : Async = async { let! state' = handle clientId (Command.Update item) return List.find (fun x -> x.id = item.id) state' } \ No newline at end of file diff --git a/samples/Tutorial/AsAt.fsx b/samples/Tutorial/AsAt.fsx index 2a2173edf..1977f123c 100644 --- a/samples/Tutorial/AsAt.fsx +++ b/samples/Tutorial/AsAt.fsx @@ -37,6 +37,8 @@ open System module Events = + let (|ForClientId|) clientId = FsCodec.StreamName.create "Account" clientId + type Delta = { count : int } type SnapshotInfo = { balanceLog : int[] } type Contract = @@ -57,7 +59,6 @@ module Events = let down (_index,e) : Contract * _ option * DateTimeOffset option = e,None,None FsCodec.NewtonsoftJson.Codec.Create(up,down) - let (|ForClientId|) clientId = Equinox.AggregateId("Account", clientId) module Fold = diff --git a/samples/Tutorial/Cosmos.fsx b/samples/Tutorial/Cosmos.fsx index c36a45bb0..cbddb1864 100644 --- a/samples/Tutorial/Cosmos.fsx +++ b/samples/Tutorial/Cosmos.fsx @@ -23,7 +23,7 @@ open System module Favorites = - let (|ForClientId|) clientId = Equinox.AggregateId("Favorites", clientId) + let (|ForClientId|) clientId = FsCodec.StreamName.create "Favorites" clientId type Item = { sku : string } type Event = diff --git a/samples/Tutorial/Counter.fsx b/samples/Tutorial/Counter.fsx index 761513756..87bd577e6 100644 --- a/samples/Tutorial/Counter.fsx +++ b/samples/Tutorial/Counter.fsx @@ -23,7 +23,7 @@ type Event = | Cleared of Cleared interface TypeShape.UnionContract.IUnionContract (* Kind of DDD aggregate ID *) -let (|ForCounterId|) (id : string) = Equinox.AggregateId("Counter", id) +let (|ForCounterId|) (id : string) = FsCodec.StreamName.create "Counter" id type State = State of int let initial : State = State 0 diff --git a/samples/Tutorial/FulfilmentCenter.fsx b/samples/Tutorial/FulfilmentCenter.fsx index 45223725b..2d093dc3e 100644 --- a/samples/Tutorial/FulfilmentCenter.fsx +++ b/samples/Tutorial/FulfilmentCenter.fsx @@ -42,6 +42,8 @@ module FulfilmentCenter = module Events = + let (|ForFcId|) id = FsCodec.StreamName.create "FulfilmentCenter" id + type AddressData = { address : Address } type ContactInformationData = { contact : ContactInformation } type FcData = { details : FcDetails } @@ -53,7 +55,6 @@ module FulfilmentCenter = | FcRenamed of FcName interface TypeShape.UnionContract.IUnionContract let codec = FsCodec.NewtonsoftJson.Codec.Create() - let (|ForFcId|) id = Equinox.AggregateId("FulfilmentCenter", id) module Fold = @@ -146,13 +147,14 @@ Log.dumpMetrics () /// Manages ingestion of summary events tagged with the version emitted from FulmentCenter.Service.QueryWithVersion module FulfilmentCenterSummary = + let (|ForFcId|) id = FsCodec.StreamName.create "FulfilmentCenterSummary" id + module Events = type UpdatedData = { version : int64; state : Summary } type Event = | Updated of UpdatedData interface TypeShape.UnionContract.IUnionContract let codec = FsCodec.NewtonsoftJson.Codec.Create() - let (|ForFcId|) id = Equinox.AggregateId("FulfilmentCenterSummary", id) type State = { version : int64; state : Types.Summary } let initial = None diff --git a/samples/Tutorial/Gapless.fs b/samples/Tutorial/Gapless.fs index fdb97529c..b16991b90 100644 --- a/samples/Tutorial/Gapless.fs +++ b/samples/Tutorial/Gapless.fs @@ -8,7 +8,7 @@ open System module Events = let [] categoryId = "Gapless" - let (|ForSequenceId|) id = Equinox.AggregateId(categoryId, SequenceId.toString id) + let (|ForSequenceId|) id = FsCodec.StreamName.create categoryId (SequenceId.toString id) type Item = { id : int64 } type Snapshotted = { reservations : int64[]; nextId : int64 } diff --git a/samples/Tutorial/Index.fs b/samples/Tutorial/Index.fs index 7688687d2..c45cd944c 100644 --- a/samples/Tutorial/Index.fs +++ b/samples/Tutorial/Index.fs @@ -4,7 +4,7 @@ module Index module Events = let [] categoryId = "Index" - let (|ForIndexId|) indexId = Equinox.AggregateId(categoryId, IndexId.toString indexId) + let (|ForIndexId|) indexId = FsCodec.StreamName.create categoryId (IndexId.toString indexId) type ItemIds = { items : string[] } type Items<'v> = { items : Map } diff --git a/samples/Tutorial/Sequence.fs b/samples/Tutorial/Sequence.fs index ade1b385d..cedd1a26c 100644 --- a/samples/Tutorial/Sequence.fs +++ b/samples/Tutorial/Sequence.fs @@ -8,7 +8,7 @@ open System module Events = let [] categoryId = "Sequence" - let (|ForSequenceId|) id = Equinox.AggregateId(categoryId, SequenceId.toString id) + let (|ForSequenceId|) id = FsCodec.StreamName.create categoryId (SequenceId.toString id) type Reserved = { next : int64 } type Event = diff --git a/samples/Tutorial/Set.fs b/samples/Tutorial/Set.fs index 6285dfdf3..4e7437b60 100644 --- a/samples/Tutorial/Set.fs +++ b/samples/Tutorial/Set.fs @@ -4,7 +4,7 @@ module Set module Events = let [] categoryId = "Set" - let (|ForSetId|) id = Equinox.AggregateId(categoryId, SetId.toString id) + let (|ForSetId|) id = FsCodec.StreamName.create categoryId (SetId.toString id) type Items = { items : string[] } type Event = diff --git a/samples/Tutorial/Todo.fsx b/samples/Tutorial/Todo.fsx index e4932a1db..cb3a3ec27 100644 --- a/samples/Tutorial/Todo.fsx +++ b/samples/Tutorial/Todo.fsx @@ -22,6 +22,8 @@ open System (* NB It's recommended to look at Favorites.fsx first as it establishes the groundwork This tutorial stresses different aspects *) +let (|ForClientId|) (id : string) = FsCodec.StreamName.create "Todos" id + type Todo = { id: int; order: int; title: string; completed: bool } type DeletedInfo = { id: int } type Snapshotted = { items: Todo[] } @@ -33,7 +35,6 @@ type Event = | Snapshotted of Snapshotted interface TypeShape.UnionContract.IUnionContract let codec = FsCodec.NewtonsoftJson.Codec.Create() -let (|ForClientId|) (id : string) = Equinox.AggregateId("Todos", id) type State = { items : Todo list; nextId : int } let initial = { items = []; nextId = 0 } diff --git a/samples/Tutorial/Upload.fs b/samples/Tutorial/Upload.fs index 715d55a1a..a37425ade 100644 --- a/samples/Tutorial/Upload.fs +++ b/samples/Tutorial/Upload.fs @@ -23,9 +23,7 @@ module UploadId = module Events = let [] categoryId = "Upload" - let (|ForCompanyAndPurchaseOrder|) (companyId, purchaseOrderId) = - let id = sprintf "%s_%s" (PurchaseOrderId.toString purchaseOrderId) (CompanyId.toString companyId) - Equinox.AggregateId(categoryId, id) + let (|ForCompanyAndPurchaseOrder|) (companyId, purchaseOrderId) = FsCodec.StreamName.compose categoryId [PurchaseOrderId.toString purchaseOrderId; CompanyId.toString companyId] type IdAssigned = { value : UploadId } type Event = diff --git a/samples/Web/Controllers/TodosController.fs b/samples/Web/Controllers/TodosController.fs index 7ed1e0b3c..fb11d3954 100644 --- a/samples/Web/Controllers/TodosController.fs +++ b/samples/Web/Controllers/TodosController.fs @@ -23,9 +23,9 @@ type GetByIdArgsTemplate = { id: int } type TodosController(service: Service) = inherit ControllerBase() - let toModel (value : TodoView) : Todo = { id = value.id; order = value.order; title = value.title; completed = value.completed } + let toModel (value : TodoView) : Events.Todo = { id = value.id; order = value.order; title = value.title; completed = value.completed } - member private __.WithUri(x : Todo) : TodoView = + member private __.WithUri(x : Events.Todo) : TodoView = let url = __.Url.RouteUrl("GetTodo", { id=x.id }, __.Request.Scheme) // Supplying scheme is secret sauce for making it absolute as required by client { id = x.id; url = url; order = x.order; title = x.title; completed = x.completed } diff --git a/src/Equinox.Cosmos/Cosmos.fs b/src/Equinox.Cosmos/Cosmos.fs index 870a3ffc0..1b2edf0f5 100644 --- a/src/Equinox.Cosmos/Cosmos.fs +++ b/src/Equinox.Cosmos/Cosmos.fs @@ -1064,12 +1064,12 @@ type Resolver<'event, 'state, 'context>(context : Context, codec, fold, initial, | Some init -> async { do! init () return! category.TrySync(log, token, originState, events, context) } } + let resolveTarget = function - | AggregateId (categoryName,streamId) -> context.ResolveContainerStream(categoryName, streamId) - | StreamName _ as x -> failwithf "Stream name not supported: %A" x + | StreamName.CategoryAndId (categoryName, streamId) -> context.ResolveContainerStream(categoryName, streamId) - member __.Resolve(target, []?option, []?context) = - match resolveTarget target, option with + member __.Resolve(streamName : StreamName, []?option, []?context) = + match resolveTarget streamName, option with | streamArgs,(None|Some AllowStale) -> resolveStream streamArgs option context | (containerStream,maybeInit),Some AssumeEmpty -> Stream.ofMemento (Token.create containerStream Position.fromKnownEmpty,initial) (resolveStream (containerStream,maybeInit) option context) diff --git a/src/Equinox.Cosmos/Equinox.Cosmos.fsproj b/src/Equinox.Cosmos/Equinox.Cosmos.fsproj index 003468cf3..f3c2a1486 100644 --- a/src/Equinox.Cosmos/Equinox.Cosmos.fsproj +++ b/src/Equinox.Cosmos/Equinox.Cosmos.fsproj @@ -25,7 +25,7 @@ - + diff --git a/src/Equinox.EventStore/Equinox.EventStore.fsproj b/src/Equinox.EventStore/Equinox.EventStore.fsproj index 462ecc3a4..109bd5464 100644 --- a/src/Equinox.EventStore/Equinox.EventStore.fsproj +++ b/src/Equinox.EventStore/Equinox.EventStore.fsproj @@ -26,7 +26,7 @@ - + diff --git a/src/Equinox.EventStore/EventStore.fs b/src/Equinox.EventStore/EventStore.fs index 4ab92fc2e..deec92de3 100644 --- a/src/Equinox.EventStore/EventStore.fs +++ b/src/Equinox.EventStore/EventStore.fs @@ -3,7 +3,6 @@ open Equinox open Equinox.Core open EventStore.ClientAPI -open FsCodec open Serilog // NB must shadow EventStore.ClientAPI.ILogger open System @@ -509,7 +508,7 @@ type CachingStrategy = | SlidingWindowPrefixed of ICache * window: TimeSpan * prefix: string type Resolver<'event, 'state, 'context> - ( context : Context, codec : IEventCodec<_,_,'context>, fold, initial, + ( context : Context, codec : FsCodec.IEventCodec<_,_,'context>, fold, initial, /// Caching can be overkill for EventStore esp considering the degree to which its intrinsic caching is a first class feature /// e.g., A key benefit is that reads of streams more than a few pages long get completed in constant time after the initial load []?caching, @@ -536,12 +535,11 @@ type Resolver<'event, 'state, 'context> | Some (CachingStrategy.SlidingWindowPrefixed(cache, window, prefix)) -> Caching.applyCacheUpdatesWithSlidingExpiration cache prefix window folder let resolveStream = Stream.create category - let resolveTarget = function AggregateId (cat,streamId) -> sprintf "%s-%s" cat streamId | StreamName streamName -> streamName let loadEmpty sn = context.LoadEmpty sn,initial - member __.Resolve(target, []?option, []?context) = - match resolveTarget target, option with - | sn,(None|Some AllowStale) -> resolveStream sn option context - | sn,Some AssumeEmpty -> Stream.ofMemento (loadEmpty sn) (resolveStream sn option context) + member __.Resolve(streamName : FsCodec.StreamName, []?option, []?context) = + match FsCodec.StreamName.toString streamName, option with + | sn, (None|Some AllowStale) -> resolveStream sn option context + | sn, Some AssumeEmpty -> Stream.ofMemento (loadEmpty sn) (resolveStream sn option context) /// Resolve from a Memento being used in a Continuation [based on position and state typically from Stream.CreateMemento] member __.FromMemento(Token.Unpack token as streamToken, state, ?context) = diff --git a/src/Equinox.MemoryStore/Equinox.MemoryStore.fsproj b/src/Equinox.MemoryStore/Equinox.MemoryStore.fsproj index c6d752a53..59a35c34e 100644 --- a/src/Equinox.MemoryStore/Equinox.MemoryStore.fsproj +++ b/src/Equinox.MemoryStore/Equinox.MemoryStore.fsproj @@ -24,7 +24,7 @@ - + \ No newline at end of file diff --git a/src/Equinox.MemoryStore/MemoryStore.fs b/src/Equinox.MemoryStore/MemoryStore.fs index dde9f9d2e..2245b8643 100644 --- a/src/Equinox.MemoryStore/MemoryStore.fs +++ b/src/Equinox.MemoryStore/MemoryStore.fs @@ -5,7 +5,6 @@ namespace Equinox.MemoryStore open Equinox open Equinox.Core -open FsCodec open System.Runtime.InteropServices /// Equivalent to GetEventStore's in purpose; signals a conflict has been detected and reprocessing of the decision will be necessary @@ -68,7 +67,7 @@ type Category<'event, 'state, 'context, 'Format>(store : VolatileStore<'Format>, member __.TrySync(_log, Token.Unpack token, state, events : 'event list, context : 'context option) = async { let inline map i (e : FsCodec.IEventData<'Format>) = FsCodec.Core.TimelineEvent.Create(int64 i,e.EventType,e.Data,e.Meta,e.CorrelationId,e.CausationId,e.Timestamp) - let encoded : ITimelineEvent<_>[] = events |> Seq.mapi (fun i e -> map (token.streamVersion+i) (codec.Encode(context,e))) |> Array.ofSeq + let encoded : FsCodec.ITimelineEvent<_>[] = events |> Seq.mapi (fun i e -> map (token.streamVersion+i) (codec.Encode(context,e))) |> Array.ofSeq let trySyncValue currentValue = if Array.length currentValue <> token.streamVersion + 1 then ConcurrentDictionarySyncResult.Conflict (token.streamVersion) else ConcurrentDictionarySyncResult.Written (Seq.append currentValue encoded |> Array.ofSeq) @@ -84,11 +83,10 @@ type Category<'event, 'state, 'context, 'Format>(store : VolatileStore<'Format>, type Resolver<'event, 'state, 'Format, 'context>(store : VolatileStore<'Format>, codec : FsCodec.IEventCodec<'event,'Format,'context>, fold, initial) = let category = Category<'event, 'state, 'context, 'Format>(store, codec, fold, initial) let resolveStream streamName context = Stream.create category streamName None context - let resolveTarget = function AggregateId (cat,streamId) -> sprintf "%s-%s" cat streamId | StreamName streamName -> streamName - member __.Resolve(target : Target, [] ?option, [] ?context : 'context) = - match resolveTarget target, option with - | sn,(None|Some AllowStale) -> resolveStream sn context - | sn,Some AssumeEmpty -> Stream.ofMemento (Token.ofEmpty sn initial) (resolveStream sn context) + member __.Resolve(streamName : FsCodec.StreamName, [] ?option, [] ?context : 'context) = + match FsCodec.StreamName.toString streamName, option with + | sn, (None|Some AllowStale) -> resolveStream sn context + | sn, Some AssumeEmpty -> Stream.ofMemento (Token.ofEmpty sn initial) (resolveStream sn context) /// Resolve from a Memento being used in a Continuation [based on position and state typically from Stream.CreateMemento] member __.FromMemento(Token.Unpack stream as streamToken, state, ?context) = diff --git a/src/Equinox.SqlStreamStore/Equinox.SqlStreamStore.fsproj b/src/Equinox.SqlStreamStore/Equinox.SqlStreamStore.fsproj index 0c75bebb6..b9b0d86c4 100644 --- a/src/Equinox.SqlStreamStore/Equinox.SqlStreamStore.fsproj +++ b/src/Equinox.SqlStreamStore/Equinox.SqlStreamStore.fsproj @@ -24,7 +24,7 @@ - + diff --git a/src/Equinox.SqlStreamStore/SqlStreamStore.fs b/src/Equinox.SqlStreamStore/SqlStreamStore.fs index d9f34bfce..2149c8d6d 100644 --- a/src/Equinox.SqlStreamStore/SqlStreamStore.fs +++ b/src/Equinox.SqlStreamStore/SqlStreamStore.fs @@ -2,7 +2,6 @@ open Equinox open Equinox.Core -open FsCodec open Serilog open System open SqlStreamStore @@ -518,7 +517,7 @@ type CachingStrategy = | SlidingWindowPrefixed of ICache * window: TimeSpan * prefix: string type Resolver<'event, 'state, 'context> - ( context : Context, codec : IEventCodec<_,_,'context>, fold, initial, + ( context : Context, codec : FsCodec.IEventCodec<_,_,'context>, fold, initial, /// Caching can be overkill for EventStore esp considering the degree to which its intrinsic caching is a first class feature /// e.g., A key benefit is that reads of streams more than a few pages long get completed in constant time after the initial load []?caching, @@ -545,12 +544,11 @@ type Resolver<'event, 'state, 'context> | Some (CachingStrategy.SlidingWindowPrefixed(cache, window, prefix)) -> Caching.applyCacheUpdatesWithSlidingExpiration cache prefix window folder let resolveStream = Stream.create category - let resolveTarget = function AggregateId (cat,streamId) -> sprintf "%s-%s" cat streamId | StreamName streamName -> streamName let loadEmpty sn = context.LoadEmpty sn,initial - member __.Resolve(target, []?option, []?context) = - match resolveTarget target, option with - | sn,(None|Some AllowStale) -> resolveStream sn option context - | sn,Some AssumeEmpty -> Stream.ofMemento (loadEmpty sn) (resolveStream sn option context) + member __.Resolve(streamName : FsCodec.StreamName, []?option, []?context) = + match FsCodec.StreamName.toString streamName, option with + | sn, (None|Some AllowStale) -> resolveStream sn option context + | sn, Some AssumeEmpty -> Stream.ofMemento (loadEmpty sn) (resolveStream sn option context) /// Resolve from a Memento being used in a Continuation [based on position and state typically from Stream.CreateMemento] member __.FromMemento(Token.Unpack token as streamToken, state, ?context) = diff --git a/src/Equinox/Equinox.fs b/src/Equinox/Equinox.fs index 7f3698555..4679b75fd 100644 --- a/src/Equinox/Equinox.fs +++ b/src/Equinox/Equinox.fs @@ -3,7 +3,7 @@ open Equinox.Core open System.Runtime.InteropServices -// Exception yielded by Stream.Transact after `count` attempts have yielded conflicts at the point of syncing with the Store +/// Exception yielded by Stream.Transact after `count` attempts have yielded conflicts at the point of syncing with the Store type MaxResyncsExhaustedException(count) = inherit exn(sprintf "Concurrency violation; aborting after %i attempts." count) @@ -37,14 +37,6 @@ type Stream<'event, 'state> /// Such a memento is then held within the application and passed in lieu of a StreamId to the StreamResolver in order to avoid having to reload state member __.CreateMemento(): Async = Flow.query(stream, log, fun syncState -> syncState.Memento) -/// Store-agnostic way to specify a target Stream to a Resolver -[] -type Target = - /// Recommended way to specify a stream identifier; a category identifier and an aggregate identity - | AggregateId of category: string * id: string - /// Specify the full stream name. NOTE use of AggregateId is recommended for simplicity and consistency. - | StreamName of streamName: string - /// Store-agnostic Context.Resolve Options type ResolveOption = /// Without consulting Cache or any other source, assume the Stream to be empty for the initial Query or Transact diff --git a/tests/Equinox.Cosmos.Integration/JsonConverterTests.fs b/tests/Equinox.Cosmos.Integration/JsonConverterTests.fs index 6fbd60f60..fc283b40c 100644 --- a/tests/Equinox.Cosmos.Integration/JsonConverterTests.fs +++ b/tests/Equinox.Cosmos.Integration/JsonConverterTests.fs @@ -16,11 +16,11 @@ type Union = let defaultSettings = FsCodec.NewtonsoftJson.Settings.CreateDefault() type Base64ZipUtf8Tests() = - let unionEncoder = FsCodec.NewtonsoftJson.Codec.Create(defaultSettings) + let eventCodec = FsCodec.NewtonsoftJson.Codec.Create(defaultSettings) [] let ``serializes, achieving compression`` () = - let encoded = unionEncoder.Encode(None,A { embed = String('x',5000) }) + let encoded = eventCodec.Encode(None,A { embed = String('x',5000) }) let e : Store.Unfold = { i = 42L c = encoded.EventType @@ -38,7 +38,7 @@ type Base64ZipUtf8Tests() = | A { embed = x } | B { embed = x } -> obj.ReferenceEquals(null, x) if hasNulls then () else - let encoded = unionEncoder.Encode(None,value) + let encoded = eventCodec.Encode(None,value) let e : Store.Unfold = { i = 42L c = encoded.EventType @@ -49,5 +49,5 @@ type Base64ZipUtf8Tests() = test <@ ser.Contains("\"d\":\"") @> let des = JsonConvert.DeserializeObject(ser) let d = FsCodec.Core.TimelineEvent.Create(-1L, des.c, des.d) - let decoded = unionEncoder.TryDecode d |> Option.get + let decoded = eventCodec.TryDecode d |> Option.get test <@ value = decoded @> \ No newline at end of file diff --git a/tests/Equinox.EventStore.Integration/Equinox.EventStore.Integration.fsproj b/tests/Equinox.EventStore.Integration/Equinox.EventStore.Integration.fsproj index e3595d660..fe5e8821f 100644 --- a/tests/Equinox.EventStore.Integration/Equinox.EventStore.Integration.fsproj +++ b/tests/Equinox.EventStore.Integration/Equinox.EventStore.Integration.fsproj @@ -22,7 +22,7 @@ - + diff --git a/tools/Equinox.Tool/Program.fs b/tools/Equinox.Tool/Program.fs index 6e830d32e..7269ae20f 100644 --- a/tools/Equinox.Tool/Program.fs +++ b/tools/Equinox.Tool/Program.fs @@ -70,7 +70,7 @@ and []StatsArguments = | Parallel _ -> "Run in Parallel (CAREFUL! can overwhelm RU allocations)." | Cosmos _ -> "Cosmos Connection parameters." and []DumpArguments = - | [] Stream of string + | [] Stream of FsCodec.StreamName | [] Correlation | [] JsonSkip | [] PrettySkip @@ -265,7 +265,7 @@ module LoadTest = | None, None -> invalidOp "impossible None, None" let clients = Array.init (a.TestsPerSecond * 2) (fun _ -> % Guid.NewGuid()) - let renderedIds = clients |> Seq.map ClientId.toStringN |> if verboseConsole then id else Seq.truncate 5 + let renderedIds = clients |> Seq.map ClientId.toString |> if verboseConsole then id else Seq.truncate 5 log.ForContext((if verboseConsole then "clientIds" else "clientIdsExcerpt"),renderedIds) .Information("Running {test} for {duration} @ {tps} hits/s across {clients} clients; Max errors: {errorCutOff}, reporting intervals: {ri}, report file: {report}", test, a.Duration, a.TestsPerSecond, clients.Length, a.ErrorCutoff, a.ReportingIntervals, reportFilename) @@ -388,10 +388,8 @@ module Dump = | _ when doJ -> System.Text.Encoding.UTF8.GetString data |> Newtonsoft.Json.Linq.JObject.Parse |> fun x -> x.ToString fo | _ -> sprintf "(%d chars)" (System.Text.Encoding.UTF8.GetString(data).Length) with e -> log.ForContext("str", System.Text.Encoding.UTF8.GetString data).Warning(e, "Parse failure"); reraise() - let readStream (name : string) = async { - let catAndId = name.Split([|'-'|],2,StringSplitOptions.RemoveEmptyEntries) - let id = match catAndId with [|cat;id|] -> Equinox.AggregateId(cat,id) | ids -> Equinox.StreamName ids.[0] - let stream = resolver.Resolve(idCodec,fold,initial,isOriginAndSnapshot) id + let readStream (streamName : FsCodec.StreamName) = async { + let stream = resolver.Resolve(idCodec,fold,initial,isOriginAndSnapshot) streamName let! _token,events = stream.Load storeLog let source = if not doE && not (List.isEmpty unfolds) then Seq.ofList unfolds else Seq.append events unfolds let mutable prevTs = None diff --git a/tools/Equinox.Tool/StoreClient.fs b/tools/Equinox.Tool/StoreClient.fs index 2ab3fdf4b..6d3f3240e 100644 --- a/tools/Equinox.Tool/StoreClient.fs +++ b/tools/Equinox.Tool/StoreClient.fs @@ -9,7 +9,7 @@ open System.Net.Http type Session(client: HttpClient, clientId: ClientId) = member __.Send(req : HttpRequestMessage) : Async = - let req = req |> HttpReq.withHeader "COMPLETELY_INSECURE_CLIENT_ID" (ClientId.toStringN clientId) + let req = req |> HttpReq.withHeader "COMPLETELY_INSECURE_CLIENT_ID" (ClientId.toString clientId) client.Send(req) type Favorited = { date: System.DateTimeOffset; skuId: SkuId } diff --git a/tools/Equinox.Tool/TodoClient.fs b/tools/Equinox.Tool/TodoClient.fs index e56a66e1a..9d03edcaa 100644 --- a/tools/Equinox.Tool/TodoClient.fs +++ b/tools/Equinox.Tool/TodoClient.fs @@ -11,7 +11,7 @@ type Todo = { id: int; url: string; order: int; title: string; completed: bool } type Session(client: HttpClient, clientId: ClientId) = member __.Send(req : HttpRequestMessage) : Async = - let req = req |> HttpReq.withHeader "COMPLETELY_INSECURE_CLIENT_ID" (ClientId.toStringN clientId) + let req = req |> HttpReq.withHeader "COMPLETELY_INSECURE_CLIENT_ID" (ClientId.toString clientId) client.Send(req) type TodosClient(session: Session) =