Transform input into simple batches

This design allows for simple two-way connections and simplifies the input design. It lacks on state resolution for state events. Given that the events are unknown and exclusively event ids are stored, this might not be relevant to implement at this level
refactor
Bram van den Heuvel 2023-11-28 15:44:13 +01:00
parent 8e647a870e
commit 327140393f
1 changed files with 216 additions and 372 deletions

View File

@ -10,43 +10,124 @@ import Internal.Tools.Iddict as Iddict exposing (Iddict)
import Internal.Tools.Filters.Main as Filter exposing (Filter) import Internal.Tools.Filters.Main as Filter exposing (Filter)
import Internal.Tools.Iddict as Iddict 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 = type alias Timeline =
{ mostRecentToken : TokenId { mostRecentToken : TokenId
, slices : Iddict Slice , slices : Iddict Slice
, tokenToId : Dict String TokenId , tokenToId : Dict TokenValue TokenId
, tokens : Iddict Token , tokens : Iddict Token
} }
{-| Pointer to a specific token.
-}
type TokenId = TokenId Int type TokenId = TokenId Int
{-| Pointer to a specific slice on the timeline.
-}
type SliceId = SliceId Int type SliceId = SliceId Int
{-| Information of a specific slice on the timeline.
-}
type Slice type Slice
= Slice = Slice
{ events : List EventId { filter : Filter
, filter : Filter , head : EventId
, next : List TokenId , next : List TokenId
, previous : 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 type Token
= Token = Token
{ next : List SliceId { next : List SliceId
, previous : List SliceId , previous : List SliceId
, head : String , head : TokenValue
, tail : List String , 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 {-| Add a new token to the timeline. If it already exists, this function does
nothing and instead returns the existing token id. nothing and instead returns the existing token id.
-} -}
addToken : Token -> Timeline -> ( TokenId, Timeline ) addToken : Token -> Timeline -> ( TokenId, Timeline )
addToken ((Token { head }) as token) timeline = addToken ((Token { head, tail }) as token) timeline =
case Dict.get head timeline.tokenToId of case getTokenIdFromToken token timeline of
Just tokenId -> 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 -> Nothing ->
insertToken token timeline insertToken token timeline
@ -71,6 +152,58 @@ addTokenAlias old new timeline =
Nothing -> Nothing ->
timeline 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. {-| Get an empty timeline.
-} -}
empty : Timeline empty : Timeline
@ -87,19 +220,45 @@ getSlice : SliceId -> Timeline -> Maybe Slice
getSlice (SliceId key) { slices } = getSlice (SliceId key) { slices } =
Iddict.get 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 getTokenFromTokenId : TokenId -> Timeline -> Maybe Token
getToken (TokenId key) { tokens } = getTokenFromTokenId (TokenId tokenId) timeline =
Iddict.get key tokens 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 getTokenFromTokenValue : TokenValue -> Timeline -> Maybe Token
getToken v timeline = getTokenFromTokenValue value timeline =
Dict.get v timeline.tokenToId 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 -> ( SliceId, Timeline )
insertSlice slice timeline = insertSlice slice timeline =
@ -107,7 +266,8 @@ insertSlice slice timeline =
|> Iddict.insert slice |> Iddict.insert slice
|> Tuple.mapBoth SliceId (\x -> { timeline | slices = x }) |> 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 -> Timeline -> ( TokenId, Timeline )
insertToken ((Token { head }) as token) 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. {-| Update an existing slice based on its id.
-} -}
mapSlice : SliceId -> (Slice -> Slice) -> Timeline -> Timeline mapSlice : SliceId -> (Slice -> Slice) -> Timeline -> Timeline
@ -132,358 +304,30 @@ mapToken : TokenId -> (Token -> Token) -> Timeline -> Timeline
mapToken (TokenId tokenId) f timeline = mapToken (TokenId tokenId) f timeline =
{ timeline | tokens = Iddict.map tokenId f timeline.tokens } { 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 ]
-- Any Timeline type contains the following pieces of information: head :: tail ->
BatchSlice (BatchToken start []) filter head tail end []
-- - `events` Comprehensive dictionary containing all locally stored timeline events {-| Turn a single token into a batch.
-- - `batches` Comprehensive dictionary containing all batches. Batches are pieces -}
-- of the timeline that have been sent by the homeserver. tokenToBatch : String -> Batch
-- - `token` Dictionary that maps for each batch token which batches it borders tokenToBatch value =
-- - `mostRecentSync` Id of the most "recent" batch in the timeline BatchToken value []
-- -}
-- 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