-
Notifications
You must be signed in to change notification settings - Fork 69
/
Copy pathSavedForLater.fs
132 lines (113 loc) · 6.21 KB
/
SavedForLater.fs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
module Domain.SavedForLater
open System
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 }
type Removed = { skus : SkuId [] }
type Merged = { items : Item [] }
module Compaction =
type Compacted = { items : Item [] }
// NB need to revise this tag if you break the unfold schema
let [<Literal>] EventType = "compacted"
type Event =
/// Checkpoint with snapshot of entire preceding event fold, avoiding need for any further reads
| [<System.Runtime.Serialization.DataMember(Name=Compaction.EventType)>] Compacted of Compaction.Compacted
/// Inclusion of another set of state in this one
| Merged of Merged
/// Removal of a set of skus
| Removed of Removed
/// Addition of a collection of skus to the list
| Added of Added
interface TypeShape.UnionContract.IUnionContract
module Utf8ArrayCodec =
let codec = FsCodec.NewtonsoftJson.Codec.Create<Event>()
module JsonElementCodec =
open FsCodec.SystemTextJson
open System.Text.Json
let private encode (options: JsonSerializerOptions) =
fun (evt: Event) ->
match evt with
| Compacted compacted -> Compaction.EventType, JsonSerializer.SerializeToElement(compacted, options)
| Merged merged -> "Merged", JsonSerializer.SerializeToElement(merged, options)
| Removed removed -> "Removed", JsonSerializer.SerializeToElement(removed, options)
| Added added -> "Added", JsonSerializer.SerializeToElement(added, options)
let private tryDecode (options: JsonSerializerOptions) =
fun (eventType, data: JsonElement) ->
match eventType with
| Compaction.EventType -> Some (Compacted <| JsonSerializer.DeserializeElement<Compaction.Compacted>(data, options))
| "Merged" -> Some (Merged <| JsonSerializer.DeserializeElement<Merged>(data, options))
| "Removed" -> Some (Removed <| JsonSerializer.DeserializeElement<Removed>(data, options))
| "Added" -> Some (Added <| JsonSerializer.DeserializeElement<Added>(data, options))
| _ -> None
let codec options = FsCodec.Codec.Create<Event, JsonElement>(encode options, tryDecode options)
module Fold =
open Events
let isSupersededAt effectiveDate (item : Item) = item.dateSaved < effectiveDate
type private InternalState(externalState : seq<Item>) =
let index = Dictionary<_,_>()
do for i in externalState do index.[i.skuId] <- i
member __.Replace (skus : seq<Item>) =
index.Clear() ; for s in skus do index.[s.skuId] <- s
member __.Append(skus : seq<Item>) =
for sku in skus do
let ok,found = index.TryGetValue sku.skuId
if not ok || found |> isSupersededAt sku.dateSaved then
index.[sku.skuId] <- sku
member __.Remove (skus : seq<SkuId>) =
for sku in skus do index.Remove sku |> ignore
member __.ToExernalState () =
index.Values |> Seq.sortBy (fun s -> -s.dateSaved.Ticks, s.skuId) |> Seq.toArray
type State = Item []
let initial = Array.empty<Item>
let fold (state : State) (events : seq<Event>) : State =
let index = InternalState state
for event in events do
match event with
| Compacted { items = skus } -> index.Replace skus
| Merged { items = skus} -> index.Append skus
| Removed { skus = skus } -> index.Remove skus
| Added { dateSaved = d; skus = skus } ->
index.Append(seq { for sku in skus -> { skuId = sku; dateSaved = d }})
index.ToExernalState()
let proposedEventsWouldExceedLimit maxSavedItems events state =
let newState = fold state events
Array.length newState > maxSavedItems
let isOrigin = function Compacted _ -> true | _ -> false
let compact state = Events.Compacted { items = state }
type Command =
| Merge of merges : Events.Item []
| Remove of skuIds : SkuId []
| Add of dateSaved : DateTimeOffset * skuIds : SkuId []
module Commands =
type private Index(state : Events.Item seq) =
let index = Dictionary<_,_>()
do for i in state do do index.[i.skuId] <- i
member __.DoesNotAlreadyContainSameOrMoreRecent effectiveDate sku =
match index.TryGetValue sku with
| true,item when item.dateSaved >= effectiveDate -> false
| _ -> true
member this.DoesNotAlreadyContainItem(item : Events.Item) =
this.DoesNotAlreadyContainSameOrMoreRecent item.dateSaved item.skuId
// yields true if the command was executed, false if it would have breached the invariants
let decide (maxSavedItems : int) (cmd : Command) (state : Fold.State) : bool * Events.Event list =
let validateAgainstInvariants events =
if Fold.proposedEventsWouldExceedLimit maxSavedItems events state then false, []
else true, events
match cmd with
| Merge merges ->
let net = merges |> Array.filter (Index state).DoesNotAlreadyContainItem
if Array.isEmpty net then true, []
else validateAgainstInvariants [ Events.Merged { items = net } ]
| Remove skuIds ->
let content = seq { for item in state -> item.skuId } |> set
let net = skuIds |> Array.filter content.Contains
if Array.isEmpty net then true, []
else true, [ Events.Removed { skus = net } ]
| Add (dateSaved, skus) ->
let index = Index state
let net = skus |> Array.filter (index.DoesNotAlreadyContainSameOrMoreRecent dateSaved)
if Array.isEmpty net then true, []
else validateAgainstInvariants [ Events.Added { skus = net ; dateSaved = dateSaved } ]