diff --git a/src/Internal/Values/Timeline.elm b/src/Internal/Values/Timeline.elm index b6a1346..fddd7bf 100644 --- a/src/Internal/Values/Timeline.elm +++ b/src/Internal/Values/Timeline.elm @@ -10,43 +10,124 @@ import Internal.Tools.Iddict as Iddict exposing (Iddict) import Internal.Tools.Filters.Main as Filter exposing (Filter) import Internal.Tools.Iddict as Iddict +{-| A batch is a piece of the timeline that can be used to update the timeline. +-} +type Batch + = BatchToken TokenValue (List TokenValue) + | BatchSlice Batch Filter EventId (List EventId) TokenValue (List TokenValue) + +{-| An event id is a raw value provided by the Matrix API. It points to an event +that is being stored elsewhere in the Matrix vault. +-} +type alias EventId = String + +{-| A TokenValue is a raw value provided by the Matrix API. It is an opaque +value which indicates a point in the timeline and provides no other information. +-} +type alias TokenValue = String + +{-| Central data type in the room. +-} type alias Timeline = { mostRecentToken : TokenId , slices : Iddict Slice - , tokenToId : Dict String TokenId + , tokenToId : Dict TokenValue TokenId , tokens : Iddict Token } +{-| Pointer to a specific token. +-} type TokenId = TokenId Int +{-| Pointer to a specific slice on the timeline. +-} type SliceId = SliceId Int +{-| Information of a specific slice on the timeline. +-} type Slice = Slice - { events : List EventId - , filter : Filter + { filter : Filter + , head : EventId , next : List TokenId , previous : List TokenId + , tail : List EventId } +{-| Information on a token, which is a point on the timeline. It might have +multiple TokenValue types point to it. +-} type Token = Token { next : List SliceId , previous : List SliceId - , head : String - , tail : List String + , head : TokenValue + , tail : List TokenValue } -type alias EventId = String +{-| Add a new batch to the timeline. Tokens that already existed, are ignored +but connected to the slices. + +The function returns token ids to where the batch starts and ends, as well as +the renewed timeline. +-} +addBatch : Batch -> Timeline -> { start : TokenId, end : TokenId, timeline : Timeline } +addBatch batch timeline = + case batch of + BatchToken head tail -> + case addToken (Token { next = [], previous = [], head = head, tail = tail }) timeline of + ( tokenId, newTimeline ) -> + { start = tokenId, end = tokenId, timeline = newTimeline } + + BatchSlice b filter sHead sTail tHead tTail -> + case addBatch b timeline of + newBatch -> + let + slice : Slice + slice = + Slice + { filter = filter + , head = sHead + , next = [] + , previous = [] + , tail = sTail + } + + token : Token + token = + Token + { next = [] + , previous = [] + , head = tHead + , tail = tTail + } + in + case newBatch.timeline |> insertSlice slice |> Tuple.mapSecond (addToken token) of + ( sliceId, ( tokenId, newTimeline ) ) -> + { start = newBatch.start + , end = tokenId + , timeline = + newTimeline + |> connectToSlice newBatch.end sliceId + |> connectToToken sliceId tokenId + } {-| Add a new token to the timeline. If it already exists, this function does nothing and instead returns the existing token id. -} addToken : Token -> Timeline -> ( TokenId, Timeline ) -addToken ((Token { head }) as token) timeline = - case Dict.get head timeline.tokenToId of +addToken ((Token { head, tail }) as token) timeline = + case getTokenIdFromToken token timeline of Just tokenId -> - ( tokenId, timeline ) + ( tokenId + , mapToken tokenId + (\(Token tk) -> + case mergeUnique ( head, tail ) ( tk.head, tk.tail ) of + ( h, t ) -> + Token { tk | head = h, tail = t } + ) + timeline + ) Nothing -> insertToken token timeline @@ -71,6 +152,58 @@ addTokenAlias old new timeline = Nothing -> timeline +{-| Connect a slice to a token to its right. The connection is established in +two directions. +-} +connectToToken : SliceId -> TokenId -> Timeline -> Timeline +connectToToken ((SliceId sliceId) as s) ((TokenId tokenId) as t) timeline = + { timeline + | slices = + Iddict.map sliceId + (\(Slice slice) -> + if isConnectedToToken t slice.next then + Slice slice + else + Slice { slice | next = t :: slice.next } + ) + timeline.slices + , tokens = + Iddict.map tokenId + (\(Token token) -> + if isConnectedToSlice s token.previous then + Token token + else + Token { token | previous = s :: token.previous } + ) + timeline.tokens + } + +{-| Connect a token to a slice to its right. The connection is established in +two directions. +-} +connectToSlice : TokenId -> SliceId -> Timeline -> Timeline +connectToSlice ((TokenId tokenId) as t) ((SliceId sliceId) as s) timeline = + { timeline + | slices = + Iddict.map sliceId + (\(Slice slice) -> + if isConnectedToToken t slice.previous then + Slice slice + else + Slice { slice | previous = t :: slice.previous } + ) + timeline.slices + , tokens = + Iddict.map tokenId + (\(Token token) -> + if isConnectedToSlice s token.next then + Token token + else + Token { token | next = s :: token.next } + ) + timeline.tokens + } + {-| Get an empty timeline. -} empty : Timeline @@ -87,19 +220,45 @@ getSlice : SliceId -> Timeline -> Maybe Slice getSlice (SliceId key) { slices } = Iddict.get key slices -{-| Get a token value from the timeline. +{-| Get a token based on its id. -} -getToken : TokenId -> Timeline -> Maybe Token -getToken (TokenId key) { tokens } = - Iddict.get key tokens +getTokenFromTokenId : TokenId -> Timeline -> Maybe Token +getTokenFromTokenId (TokenId tokenId) timeline = + Iddict.get tokenId timeline.tokens -{-| Get the token id of an existing token value. +{-| Get a token based on its token value. -} -getTokenId : String -> Timeline -> Maybe TokenId -getToken v timeline = - Dict.get v timeline.tokenToId +getTokenFromTokenValue : TokenValue -> Timeline -> Maybe Token +getTokenFromTokenValue value timeline = + timeline + |> getTokenIdFromTokenValue value + |> Maybe.andThen (\tid -> getTokenFromTokenId tid timeline) -{-| Insert a new slice into the timeline. +{-| Get the token id based on a token value. The function returns Nothing if it +isn't on the timeline. +-} +getTokenIdFromTokenValue : TokenValue -> Timeline -> Maybe TokenId +getTokenIdFromTokenValue value timeline = + Dict.get value timeline.tokenToId + +{-| Get the token's id. The function returns Nothing if the token isn't on the +timeline. +-} +getTokenIdFromToken : Token -> Timeline -> Maybe TokenId +getTokenIdFromToken (Token { head, tail }) timeline = + List.foldl + (\value ptr -> + case ptr of + Nothing -> + getTokenIdFromTokenValue value timeline + + Just _ -> + ptr + ) + Nothing (head :: tail) + +{-| Insert a new slice into the timeline. This is a raw operation that should +never be exposed! -} insertSlice : Slice -> Timeline -> ( SliceId, Timeline ) insertSlice slice timeline = @@ -107,7 +266,8 @@ insertSlice slice timeline = |> Iddict.insert slice |> Tuple.mapBoth SliceId (\x -> { timeline | slices = x }) -{-| Insert a new token into the timeline. +{-| Insert a new token into the timeline. This is a raw operation that should +never be exposed! -} insertToken : Token -> Timeline -> ( TokenId, Timeline ) insertToken ((Token { head }) as token) timeline = @@ -120,6 +280,18 @@ insertToken ((Token { head }) as token) timeline = } ) +{-| Whether a list contains a given slice id. +-} +isConnectedToSlice : SliceId -> List SliceId -> Bool +isConnectedToSlice (SliceId a) = + List.any (\(SliceId b) -> a == b) + +{-| Whether a list contains a given token id. +-} +isConnectedToToken : TokenId -> List TokenId -> Bool +isConnectedToToken (TokenId a) = + List.any (\(TokenId b) -> a == b) + {-| Update an existing slice based on its id. -} mapSlice : SliceId -> (Slice -> Slice) -> Timeline -> Timeline @@ -132,358 +304,30 @@ mapToken : TokenId -> (Token -> Token) -> Timeline -> Timeline mapToken (TokenId tokenId) f timeline = { timeline | tokens = Iddict.map tokenId f timeline.tokens } +{-| Merge two lists such that each element only appears once. +-} +mergeUnique : (a, List a) -> (a, List a) -> (a, List a) +mergeUnique (head, tail) (otherHead, otherTail) = + otherTail + |> List.filter (\e -> e /= otherHead) + |> (::) otherHead + |> List.filter (\e -> e /= head) + |> List.filter (\e -> not <| List.member e tail ) + |> Tuple.pair head --- {-| The Timeline is a comprehensive object describing a timeline in a room. +{-| Turn a single slice into a batch. +-} +sliceToBatch : { start : String, filter : Filter, events : List EventId, end : String } -> Batch +sliceToBatch { start, filter, events, end } = + case events of + [] -> + BatchToken end [ start ] + + head :: tail -> + BatchSlice (BatchToken start []) filter head tail end [] --- Any Timeline type contains the following pieces of information: - --- - `events` Comprehensive dictionary containing all locally stored timeline events --- - `batches` Comprehensive dictionary containing all batches. Batches are pieces --- of the timeline that have been sent by the homeserver. --- - `token` Dictionary that maps for each batch token which batches it borders --- - `mostRecentSync` Id of the most "recent" batch in the timeline --- -} --- type Timeline --- = Timeline --- { events : Hashdict IEvent --- , batches : Iddict TimelineBatch --- , token : DefaultDict String (List Int) --- , mostRecentSync : Maybe Int --- } - --- {-| A BatchToken is a token that has been handed out by the server to mark the end of a -} --- type alias BatchToken = String - --- type alias TimelineBatch = --- { prevBatch : List Batch --- , nextBatch : List Batch --- , filter : Filter --- , events : List String --- , stateDelta : StateManager --- } - --- type Batch --- = Token BatchToken --- | Batch Int - --- addNewSync : --- { events : List IEvent --- , filter : Filter --- , limited : Bool --- , nextBatch : String --- , prevBatch : String --- , stateDelta : Maybe StateManager --- } -> Timeline -> Timeline --- addNewSync data (Timeline timeline) = --- let --- batchToInsert : TimelineBatch --- batchToInsert = --- { prevBatch = --- [ Just <| Token data.prevBatch --- , Maybe.map Batch timeline.mostRecentSync --- ] --- |> List.filterMap identity --- , nextBatch = --- [ Token data.nextBatch ] --- , filter = data.filter --- , events = List.map Event.eventId data.events --- , stateDelta = Maybe.withDefault StateManager.empty data.stateDelta --- } --- in --- case Iddict.insert batchToInsert timeline.batches of --- ( batchId, batches ) -> --- Timeline --- { events = List.foldl Hashdict.insert timeline.events data.events --- , batches = batches --- , mostRecentSync = Just batchId --- , token = --- timeline.token --- |> DefaultDict.update data.prevBatch --- (\value -> --- case value of --- Just v -> --- Just (batchId :: v) --- Nothing -> --- Just [ batchId ] --- ) --- |> DefaultDict.update data.nextBatch --- (\value -> --- case value of --- Just v -> --- Just (batchId :: v) --- Nothing -> --- Just [ batchId ] --- ) --- } - --- -- type Timeline --- -- = Timeline --- -- { prevBatch : String --- -- , nextBatch : String --- -- , events : List IEvent --- -- , stateAtStart : StateManager --- -- , previous : BeforeTimeline --- -- } - - --- type BeforeTimeline --- = Endless String --- | Gap Timeline --- | StartOfTimeline - - --- {-| Add a new batch of events to the front of the timeline. --- -} --- addNewEvents : --- { events : List IEvent --- , limited : Bool --- , nextBatch : String --- , prevBatch : String --- , stateDelta : Maybe StateManager --- } --- -> Timeline --- -> Timeline --- addNewEvents { events, limited, nextBatch, prevBatch, stateDelta } (Timeline t) = --- Timeline --- (if prevBatch == t.nextBatch || not limited then --- { t --- | events = t.events ++ events --- , nextBatch = nextBatch --- } - --- else --- { prevBatch = prevBatch --- , nextBatch = nextBatch --- , events = events --- , stateAtStart = --- t --- |> Timeline --- |> mostRecentState --- |> StateManager.updateRoomStateWith --- (stateDelta --- |> Maybe.withDefault StateManager.empty --- ) --- , previous = Gap (Timeline t) --- } --- ) - - --- {-| Create a new timeline. --- -} --- newFromEvents : --- { events : List IEvent --- , nextBatch : String --- , prevBatch : Maybe String --- , stateDelta : Maybe StateManager --- } --- -> Timeline --- newFromEvents { events, nextBatch, prevBatch, stateDelta } = --- Timeline --- { events = events --- , nextBatch = nextBatch --- , prevBatch = --- prevBatch --- |> Maybe.withDefault Leaking.prevBatch --- , previous = --- prevBatch --- |> Maybe.map Endless --- |> Maybe.withDefault StartOfTimeline --- , stateAtStart = --- stateDelta --- |> Maybe.withDefault StateManager.empty --- } - - --- {-| Insert events starting from a known batch token. --- -} --- insertEvents : --- { events : List IEvent --- , nextBatch : String --- , prevBatch : Maybe String --- , stateDelta : Maybe StateManager --- } --- -> Timeline --- -> Timeline --- insertEvents ({ events, nextBatch, prevBatch, stateDelta } as data) (Timeline t) = --- Timeline --- (case prevBatch of --- -- No prevbatch suggests the start of the timeline. --- -- This means that we must recurse until we've hit the bottom, --- -- and then mark the bottom of the timeline. --- Nothing -> --- case t.previous of --- Gap prevT -> --- { t --- | previous = --- prevT --- |> insertEvents data --- |> Gap --- } - --- _ -> --- if nextBatch == t.prevBatch then --- { t | previous = StartOfTimeline, events = events ++ t.events, stateAtStart = StateManager.empty } - --- else --- { t | previous = Gap <| newFromEvents data } - --- -- If there is a prevbatch, it is not the start of the timeline --- -- and could be located anywhere. --- -- Starting at the front, look for a way to match it with the existing timeline. --- Just p -> --- -- Piece connects to the front of the timeline. --- if t.nextBatch == p then --- { t --- | events = t.events ++ events --- , nextBatch = nextBatch --- } --- -- Piece connects to the back of the timeline. - --- else if nextBatch == t.prevBatch then --- case t.previous of --- Gap (Timeline prevT) -> --- -- Piece also connects to the timeline in the back, --- -- allowing the two timelines to merge. --- if prevT.nextBatch == p then --- { events = prevT.events ++ events ++ t.events --- , nextBatch = t.nextBatch --- , prevBatch = prevT.prevBatch --- , stateAtStart = prevT.stateAtStart --- , previous = prevT.previous --- } - --- else --- { t --- | events = events ++ t.events --- , prevBatch = p --- , stateAtStart = --- stateDelta --- |> Maybe.withDefault StateManager.empty --- } - --- Endless _ -> --- { t --- | events = events ++ t.events --- , prevBatch = p --- , stateAtStart = --- stateDelta --- |> Maybe.withDefault StateManager.empty --- , previous = Endless p --- } - --- _ -> --- { t --- | events = events ++ t.events --- , prevBatch = p --- , stateAtStart = --- stateDelta --- |> Maybe.withDefault StateManager.empty --- } --- -- Piece doesn't connect to this piece of the timeline. --- -- Consequently, look for previous parts of the timeline to see if it connects. - --- else --- case t.previous of --- Gap prevT -> --- { t --- | previous = --- prevT --- |> insertEvents data --- |> Gap --- } - --- _ -> --- t --- ) - - --- {-| Get the width of the latest gap. This data is usually accessed when trying to get more messages. --- -} --- latestGap : Timeline -> Maybe { from : Maybe String, to : String } --- latestGap (Timeline t) = --- case t.previous of --- StartOfTimeline -> --- Nothing - --- Endless prevBatch -> --- Just { from = Nothing, to = prevBatch } - --- Gap (Timeline pt) -> --- Just { from = Just pt.nextBatch, to = t.prevBatch } - - --- {-| Get the longest uninterrupted length of most recent events. --- -} --- localSize : Timeline -> Int --- localSize = --- mostRecentEvents >> List.length - - --- {-| Get a list of the most recent events recorded. --- -} --- mostRecentEvents : Timeline -> List IEvent --- mostRecentEvents (Timeline t) = --- t.events - - --- {-| Get the needed `since` parameter to get the latest events. --- -} --- nextSyncToken : Timeline -> String --- nextSyncToken (Timeline t) = --- t.nextBatch - - --- {-| Get the state of the room after the most recent event. --- -} --- mostRecentState : Timeline -> StateManager --- mostRecentState (Timeline t) = --- t.stateAtStart --- |> StateManager.updateRoomStateWith --- (StateManager.fromEventList t.events) - - --- {-| Get the timeline's room state at any given event. The function returns `Nothing` if the event is not found in the timeline. --- -} --- stateAtEvent : IEvent -> Timeline -> Maybe StateManager --- stateAtEvent event (Timeline t) = --- if --- t.events --- |> List.map Event.eventId --- |> List.member (Event.eventId event) --- then --- Fold.untilCompleted --- List.foldl --- (\e -> --- StateManager.addEvent e --- >> (if Event.eventId e == Event.eventId event then --- Fold.AnswerWith - --- else --- Fold.ContinueWith --- ) --- ) --- t.stateAtStart --- t.events --- |> Just - --- else --- case t.previous of --- Gap prevT -> --- stateAtEvent event prevT - --- _ -> --- Nothing - - --- {-| Count how many events the current timeline is storing. --- -} --- size : Timeline -> Int --- size (Timeline t) = --- (case t.previous of --- Gap prev -> --- size prev - --- _ -> --- 0 --- ) --- + List.length t.events +{-| Turn a single token into a batch. +-} +tokenToBatch : String -> Batch +tokenToBatch value = + BatchToken value []