Add first design of timeline

The current unfinished design is a first implementation of how the timeline could be built properly. It yet needs a simple way of adding data types, however, which should be a bit more universal.
refactor
Bram van den Heuvel 2023-11-27 13:57:41 +01:00
parent a68253cc43
commit 8e647a870e
1 changed files with 453 additions and 336 deletions

View File

@ -6,367 +6,484 @@ a simple way to view events.
-}
import FastDict as Dict exposing (Dict)
import Internal.Config.Leaking as Leaking
import Internal.Tools.Fold as Fold
import Internal.Values.Event as Event exposing (IEvent)
import Internal.Values.StateManager as StateManager exposing (StateManager)
import Internal.Tools.DefaultDict as DefaultDict exposing (DefaultDict)
import Internal.Tools.Hashdict as Hashdict exposing (Hashdict)
import Internal.Tools.Iddict as Iddict exposing (Iddict)
import Internal.Tools.Filters.Main as Filter exposing (Filter)
import Internal.Config.Leaking exposing (nextBatch)
import Internal.Tools.Iddict as Iddict
{-| The Timeline is a comprehensive object describing a timeline in a room.
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 alias Timeline =
{ mostRecentToken : TokenId
, slices : Iddict Slice
, tokenToId : Dict String TokenId
, tokens : Iddict Token
}
type Batch
= Token BatchToken
| Batch Int
type TokenId = TokenId 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 SliceId = SliceId Int
type Slice
= Slice
{ events : List EventId
, filter : Filter
, next : List TokenId
, previous : List TokenId
}
type Token
= Token
{ next : List SliceId
, previous : List SliceId
, head : String
, tail : List String
}
type alias EventId = String
{-| 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
Just tokenId ->
( tokenId, timeline )
Nothing ->
insertToken token timeline
{-| Sometimes two separate tokens point to the same location in the timeline.
You can add a new token value as an alias to the timeline using this function.
-}
addTokenAlias : String -> String -> Timeline -> Timeline
addTokenAlias old new timeline =
case Dict.get old timeline.tokenToId of
Just tokenId ->
timeline
-- Update the token
|> mapToken
tokenId
(\(Token t) ->
Token { t | head = new, tail = t.head :: t.tail }
)
-- Add a token pointer for the new value
|> (\tl -> { tl | tokenToId = Dict.insert new tokenId tl.tokenToId })
Nothing ->
timeline
{-| Get an empty timeline.
-}
empty : Timeline
empty =
{ mostRecentToken = TokenId 0
, slices = Iddict.empty
, tokenToId = Dict.empty
, tokens = Iddict.empty
}
{-| Get a slice of events from the timeline.
-}
getSlice : SliceId -> Timeline -> Maybe Slice
getSlice (SliceId key) { slices } =
Iddict.get key slices
{-| Get a token value from the timeline.
-}
getToken : TokenId -> Timeline -> Maybe Token
getToken (TokenId key) { tokens } =
Iddict.get key tokens
{-| Get the token id of an existing token value.
-}
getTokenId : String -> Timeline -> Maybe TokenId
getToken v timeline =
Dict.get v timeline.tokenToId
{-| Insert a new slice into the timeline.
-}
insertSlice : Slice -> Timeline -> ( SliceId, Timeline )
insertSlice slice timeline =
timeline.slices
|> Iddict.insert slice
|> Tuple.mapBoth SliceId (\x -> { timeline | slices = x })
{-| Insert a new token into the timeline.
-}
insertToken : Token -> Timeline -> ( TokenId, Timeline )
insertToken ((Token { head }) as token) timeline =
case Iddict.insert token timeline.tokens of
( tokenId, tokens ) ->
( TokenId tokenId
, { timeline
| tokenToId = Dict.insert head (TokenId tokenId) timeline.tokenToId
, tokens = tokens
}
)
{-| Update an existing slice based on its id.
-}
mapSlice : SliceId -> (Slice -> Slice) -> Timeline -> Timeline
mapSlice (SliceId sliceId) f timeline =
{ timeline | slices = Iddict.map sliceId f timeline.slices }
{-| Update an existing token based on its id.
-}
mapToken : TokenId -> (Token -> Token) -> Timeline -> Timeline
mapToken (TokenId tokenId) f timeline =
{ timeline | tokens = Iddict.map tokenId f timeline.tokens }
-- {-| The Timeline is a comprehensive object describing a timeline in a room.
-- 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
-- { prevBatch : String
-- , nextBatch : String
-- , events : List IEvent
-- , stateAtStart : StateManager
-- , previous : BeforeTimeline
-- { 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
-- }
type BeforeTimeline
= Endless String
| Gap Timeline
| StartOfTimeline
-- {-| 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
-- )
{-| 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
}
-- {-| 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
else
{ prevBatch = prevBatch
, nextBatch = nextBatch
, events = events
, stateAtStart =
t
|> Timeline
|> mostRecentState
|> StateManager.updateRoomStateWith
(stateDelta
|> Maybe.withDefault StateManager.empty
)
, previous = Gap (Timeline t)
}
)
-- Endless prevBatch ->
-- Just { from = Nothing, to = prevBatch }
-- Gap (Timeline pt) ->
-- Just { from = Just pt.nextBatch, to = t.prevBatch }
{-| 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
}
-- {-| Get the longest uninterrupted length of most recent events.
-- -}
-- localSize : Timeline -> Int
-- localSize =
-- mostRecentEvents >> List.length
{-| 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 a list of the most recent events recorded.
-- -}
-- mostRecentEvents : Timeline -> List IEvent
-- mostRecentEvents (Timeline t) =
-- t.events
{-| 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 needed `since` parameter to get the latest events.
-- -}
-- nextSyncToken : Timeline -> String
-- nextSyncToken (Timeline t) =
-- t.nextBatch
{-| Get the longest uninterrupted length of most recent events.
-}
localSize : Timeline -> Int
localSize =
mostRecentEvents >> List.length
-- {-| 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 a list of the most recent events recorded.
-}
mostRecentEvents : Timeline -> List IEvent
mostRecentEvents (Timeline t) =
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
{-| Get the needed `since` parameter to get the latest events.
-}
nextSyncToken : Timeline -> String
nextSyncToken (Timeline t) =
t.nextBatch
-- {-| Count how many events the current timeline is storing.
-- -}
-- size : Timeline -> Int
-- size (Timeline t) =
-- (case t.previous of
-- Gap prev ->
-- size prev
{-| 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
-- _ ->
-- 0
-- )
-- + List.length t.events