diff --git a/src/Applications/Brain/User/State.elm b/src/Applications/Brain/User/State.elm index f9456af6..13a5c261 100644 --- a/src/Applications/Brain/User/State.elm +++ b/src/Applications/Brain/User/State.elm @@ -437,8 +437,11 @@ saveHypaethralDataBitsTask bits userData saveFn = one part at a time. -} saveHypaethralDataBits : List HypaethralBit -> Manager -saveHypaethralDataBits bits model = +saveHypaethralDataBits bitsWithoutModifiedAt model = let + bits = + ModifiedAt :: bitsWithoutModifiedAt + userData = model.hypaethralUserData diff --git a/src/Applications/UI/User/State/Export.elm b/src/Applications/UI/User/State/Export.elm index 5ba33f6c..1a685206 100644 --- a/src/Applications/UI/User/State/Export.elm +++ b/src/Applications/UI/User/State/Export.elm @@ -76,6 +76,12 @@ saveFavourites model = |> return model +saveModifiedAt : Manager +saveModifiedAt = + -- Handled by 🧠 + Return.singleton + + savePlaylists : Manager savePlaylists model = model.playlists diff --git a/src/Applications/UI/User/State/Import.elm b/src/Applications/UI/User/State/Import.elm index f4a6ca36..f324d977 100644 --- a/src/Applications/UI/User/State/Import.elm +++ b/src/Applications/UI/User/State/Import.elm @@ -177,6 +177,9 @@ saveAllHypaethralData return = Favourites -> andThen User.saveFavourites + ModifiedAt -> + andThen User.saveModifiedAt + Playlists -> andThen User.savePlaylists diff --git a/src/Javascript/Brain/common.ts b/src/Javascript/Brain/common.ts index 8217afcc..37c563ae 100644 --- a/src/Javascript/Brain/common.ts +++ b/src/Javascript/Brain/common.ts @@ -83,6 +83,9 @@ export function decryptIfNeeded(data) { } else if (data.startsWith("{") || data.startsWith("[")) { return Promise.resolve(data) + } else if (data.length < 15 && Number.isInteger(parseInt(data, 10))) { + return Promise.resolve(data) + } else { return data ? getSecretKey().then(secretKey => { diff --git a/src/Library/Syncing.elm b/src/Library/Syncing.elm index 5fbbdd47..46a17670 100644 --- a/src/Library/Syncing.elm +++ b/src/Library/Syncing.elm @@ -1,4 +1,4 @@ -module Syncing exposing (..) +module Syncing exposing (LocalConfig, RemoteConfig, task) import Json.Decode as Decode import Json.Encode as Json @@ -6,62 +6,118 @@ import Maybe.Extra as Maybe import Task exposing (Task) import Task.Extra as Task import Time +import Time.Ext as Time import User.Layer as User exposing (..) + +-- 🌳 + + +type alias LocalConfig = + { localData : HypaethralData + , saveLocal : HypaethralBit -> Decode.Value -> Task String () + } + + +type alias RemoteConfig = + { retrieve : HypaethralBit -> Task String (Maybe Decode.Value) + , save : HypaethralBit -> Decode.Value -> Task String () + } + + + +-- 🛠 + + {-| Syncs all hypaethral data. + +Returns `Nothing` if the local data is preferred. + +🏝️ LOCAL +🛰️ REMOTE + +1. Try to pull remote `modified.json` timestamp + a. If newer, continue (#2) + b. If same, do nothing + c. If older, or not present, prefer local data 🏝️ (stop & push) +2. Try to download all remote data + a. If any remote data, continue (#3) + b. If none, prefer local data 🏝️ (stop & push) +3. Decode remote data and compare timestamps + a. If newer, use remote data 🛰️ + b. If same, do nothing + c. If older, prefer local data 🏝️ (stop & push) + d. If no timestamps, if local data, prefer local 🏝️ (stop & push), otherwise remote 🛰️ + -} task : Task String a - -> - { localData : HypaethralData - , saveLocal : HypaethralBit -> Decode.Value -> Task String () - } - -> - { retrieve : HypaethralBit -> Task String (Maybe Decode.Value) - , save : HypaethralBit -> Decode.Value -> Task String () - } + -> LocalConfig + -> RemoteConfig + -> Task String (Maybe HypaethralData) +task initialTask localConfig remoteConfig = + initialTask + |> Task.andThen + (\_ -> + remoteConfig.retrieve ModifiedAt + ) + |> Task.andThen + (\maybeModifiedAt -> + let + maybeRemoteModifiedAt = + Maybe.andThen + (Decode.decodeValue Time.decoder >> Result.toMaybe) + maybeModifiedAt + in + case ( maybeRemoteModifiedAt, localConfig.localData.modifiedAt ) of + ( Just remoteModifiedAt, Just localModifiedAt ) -> + if Time.posixToMillis remoteModifiedAt == Time.posixToMillis localModifiedAt then + -- 🏝️ + Task.succeed Nothing + + else if Time.posixToMillis remoteModifiedAt > Time.posixToMillis localModifiedAt then + -- 🛰️ + fetchRemote localConfig remoteConfig + + else + -- 🏝️ → 🛰️ + pushLocalToRemote localConfig remoteConfig { return = Nothing } + + ( Just _, Nothing ) -> + -- 🛰️ + fetchRemote localConfig remoteConfig + + ( Nothing, _ ) -> + -- 🛰️ + fetchRemote localConfig remoteConfig + ) + + +fetchRemote : + LocalConfig + -> RemoteConfig -> Task String (Maybe HypaethralData) -task initialTask { localData, saveLocal } { retrieve, save } = - -- 1. Check if any existing data is present on the service to sync with. - -- 2. If not, copy over all current data (in memory) to that service. - -- If so: 👇 - -- 3. If no data is present locally then just load the remote data (ie. service data) - -- No data = no sources, favourites & playlists - -- If so: 👇 - -- 4. Compare modifiedAt timestamps - -- (if no remote timestamp is available try to calculate it based on the data, progress → favourites → playlists → tracks → sources) - -- If remote is newer: Load remote data - -- Otherwise: 👇 - -- 5. Load remote data and run merge function for each type of data (sources, tracks, etc.) - -- 6. Store merged data into memory - -- 7. Overwrite remote data - -- - -- 🏝️ LOCAL - -- 🛰️ REMOTE +fetchRemote localConfig remoteConfig = let - noLocalData = - List.isEmpty localData.sources - && List.isEmpty localData.favourites - && List.isEmpty localData.playlists + { localData, saveLocal } = + localConfig - pushLocalToRemote { return } = - localData - |> User.encodedHypaethralDataList - |> List.map (\( bit, data ) -> save bit data) - |> Task.sequence - |> Task.map (\_ -> return) + { retrieve } = + remoteConfig saveLocally data = data |> User.saveHypaethralData saveLocal |> Task.map (\_ -> Just data) + + noLocalData = + List.isEmpty localData.sources + && List.isEmpty localData.favourites + && List.isEmpty localData.playlists in - initialTask - |> Task.andThen - (\_ -> - User.retrieveHypaethralData retrieve - ) + retrieve + |> User.retrieveHypaethralData |> Task.andThen (\list -> let @@ -74,7 +130,7 @@ task initialTask { localData, saveLocal } { retrieve, save } = else -- 🏝️ → 🛰️ - pushLocalToRemote { return = list } + pushLocalToRemote localConfig remoteConfig { return = list } ) |> Task.andThen (\list -> @@ -100,16 +156,16 @@ task initialTask { localData, saveLocal } { retrieve, save } = saveLocally remoteData else - -- 🏝️ - pushLocalToRemote { return = Nothing } + -- 🏝️ → 🛰️ + pushLocalToRemote localConfig remoteConfig { return = Nothing } ( Just _, Nothing ) -> -- 🛰️ saveLocally remoteData ( Nothing, Just _ ) -> - -- 🏝️ - pushLocalToRemote { return = Nothing } + -- 🏝️ → 🛰️ + pushLocalToRemote localConfig remoteConfig { return = Nothing } _ -> if noLocalData then @@ -120,3 +176,16 @@ task initialTask { localData, saveLocal } { retrieve, save } = -- 🏝️ Task.succeed Nothing ) + + + +-- ㊙️ + + +pushLocalToRemote : LocalConfig -> RemoteConfig -> { return : a } -> Task String a +pushLocalToRemote localConfig remoteConfig { return } = + localConfig.localData + |> User.encodedHypaethralDataList + |> List.map (\( bit, data ) -> remoteConfig.save bit data) + |> Task.sequence + |> Task.map (\_ -> return) diff --git a/src/Library/User/Layer.elm b/src/Library/User/Layer.elm index 9d98bc5d..3bbbb8fc 100644 --- a/src/Library/User/Layer.elm +++ b/src/Library/User/Layer.elm @@ -98,6 +98,8 @@ type HypaethralBit | Settings | Sources | Tracks + -- + | ModifiedAt type HypaethralBaggage @@ -316,31 +318,39 @@ emptyHypaethralData = encodeHypaethralBit : HypaethralBit -> HypaethralData -> Json.Value encodeHypaethralBit bit { favourites, playlists, progress, settings, sources, tracks, modifiedAt } = - Json.Encode.object - [ ( "data" - , case bit of - Favourites -> - Json.Encode.list Tracks.encodeFavourite favourites + case bit of + ModifiedAt -> + Maybe.unwrap Json.Encode.null Time.encode modifiedAt - Playlists -> - Json.Encode.list Playlists.encode playlists + _ -> + Json.Encode.object + [ ( "data" + , case bit of + Favourites -> + Json.Encode.list Tracks.encodeFavourite favourites - Progress -> - Json.Encode.dict identity Json.Encode.float progress + ModifiedAt -> + Maybe.unwrap Json.Encode.null Time.encode modifiedAt - Settings -> - Maybe.unwrap Json.Encode.null Settings.encode settings + Playlists -> + Json.Encode.list Playlists.encode playlists - Sources -> - Json.Encode.list Sources.encode sources + Progress -> + Json.Encode.dict identity Json.Encode.float progress - Tracks -> - Json.Encode.list Tracks.encodeTrack tracks - ) - , ( "modifiedAt" - , Maybe.unwrap Json.Encode.null Time.encode modifiedAt - ) - ] + Settings -> + Maybe.unwrap Json.Encode.null Settings.encode settings + + Sources -> + Json.Encode.list Sources.encode sources + + Tracks -> + Json.Encode.list Tracks.encodeTrack tracks + ) + , ( "modifiedAt" + , Maybe.unwrap Json.Encode.null Time.encode modifiedAt + ) + ] encodeHypaethralData : HypaethralData -> Json.Value @@ -376,6 +386,9 @@ hypaethralBitKey bit = Favourites -> "favourites" + ModifiedAt -> + "modified" + Playlists -> "playlists" @@ -402,7 +415,7 @@ hypaethralDataDecoder = { data = def, modifiedAt = Nothing } a in - (\fav pla pro set sor tra -> + (\fav pla pro set sor tra mod -> { favourites = fav.data , playlists = pla.data , progress = pro.data @@ -412,17 +425,22 @@ hypaethralDataDecoder = -- , modifiedAt = - [ fav.modifiedAt - , pla.modifiedAt - , pro.modifiedAt - , set.modifiedAt - , sor.modifiedAt - , tra.modifiedAt - ] - |> List.filterMap (Maybe.map Time.posixToMillis) - |> List.sort - |> List.last - |> Maybe.map Time.millisToPosix + case mod of + Just m -> + Just (Time.millisToPosix m) + + Nothing -> + [ fav.modifiedAt + , pla.modifiedAt + , pro.modifiedAt + , set.modifiedAt + , sor.modifiedAt + , tra.modifiedAt + ] + |> List.filterMap (Maybe.map Time.posixToMillis) + |> List.sort + |> List.last + |> Maybe.map Time.millisToPosix } ) |> Json.succeed @@ -432,6 +450,7 @@ hypaethralDataDecoder = |> optionalWithPossiblyData Settings (Json.maybe Settings.decoder) Nothing |> optionalWithPossiblyData Sources (Json.listIgnore Sources.decoder) [] |> optionalWithPossiblyData Tracks (Json.listIgnore Tracks.trackDecoder) [] + |> optional (hypaethralBitKey ModifiedAt) (Json.maybe Json.int) Nothing