A music player that connects to your cloud/distributed storage.
1module Syncing exposing (LocalConfig, RemoteConfig, task)
2
3import Json.Decode as Decode
4import Json.Encode as Json
5import Maybe.Extra as Maybe
6import Task exposing (Task)
7import Task.Extra as Task
8import Time
9import Time.Ext as Time
10import User.Layer as User exposing (..)
11
12
13
14-- 🌳
15
16
17type alias LocalConfig =
18 { localData : HypaethralData
19 , saveLocal : HypaethralBit -> Decode.Value -> Task String ()
20 }
21
22
23type alias RemoteConfig =
24 { retrieve : HypaethralBit -> Task String (Maybe Decode.Value)
25 , save : HypaethralBit -> Decode.Value -> Task String ()
26 }
27
28
29
30-- 🛠
31
32
33{-| Syncs all hypaethral data.
34
35Returns `Nothing` if the local data is preferred.
36
37🏝️ LOCAL
38🛰️ REMOTE
39
401. Try to pull remote `modified.json` timestamp
41 a. If newer, continue (#2)
42 b. If same, do nothing
43 c. If older, or not present, prefer local data 🏝️ (stop & push)
442. Try to download all remote data
45 a. If any remote data, continue (#3)
46 b. If none, prefer local data 🏝️ (stop & push)
473. Decode remote data and compare timestamps
48 a. If newer, use remote data 🛰️
49 b. If same, do nothing
50 c. If older, prefer local data 🏝️ (stop & push)
51 d. If no timestamps, if local data, prefer local 🏝️ (stop & push), otherwise remote 🛰️
52
53-}
54task :
55 Task String a
56 -> LocalConfig
57 -> RemoteConfig
58 -> Task String (Maybe HypaethralData)
59task initialTask localConfig remoteConfig =
60 initialTask
61 |> Task.andThen
62 (\_ ->
63 remoteConfig.retrieve ModifiedAt
64 )
65 |> Task.andThen
66 (\maybeModifiedAt ->
67 let
68 maybeRemoteModifiedAt =
69 Maybe.andThen
70 (Decode.decodeValue Time.decoder >> Result.toMaybe)
71 maybeModifiedAt
72 in
73 case ( maybeRemoteModifiedAt, localConfig.localData.modifiedAt ) of
74 ( Just remoteModifiedAt, Just localModifiedAt ) ->
75 if Time.posixToMillis remoteModifiedAt == Time.posixToMillis localModifiedAt then
76 -- 🏝️
77 Task.succeed Nothing
78
79 else if Time.posixToMillis remoteModifiedAt > Time.posixToMillis localModifiedAt then
80 -- 🛰️
81 fetchRemote localConfig remoteConfig
82
83 else
84 -- 🏝️ → 🛰️
85 pushLocalToRemote localConfig remoteConfig { return = Nothing }
86
87 ( Just _, Nothing ) ->
88 -- 🛰️
89 fetchRemote localConfig remoteConfig
90
91 ( Nothing, _ ) ->
92 -- 🛰️
93 fetchRemote localConfig remoteConfig
94 )
95
96
97fetchRemote :
98 LocalConfig
99 -> RemoteConfig
100 -> Task String (Maybe HypaethralData)
101fetchRemote localConfig remoteConfig =
102 let
103 { localData, saveLocal } =
104 localConfig
105
106 { retrieve } =
107 remoteConfig
108
109 saveLocally data =
110 data
111 |> User.saveHypaethralData saveLocal
112 |> Task.map (\_ -> Just data)
113
114 noLocalData =
115 List.isEmpty localData.sources
116 && List.isEmpty localData.favourites
117 && List.isEmpty localData.playlists
118 in
119 retrieve
120 |> User.retrieveHypaethralData
121 |> Task.andThen
122 (\list ->
123 let
124 remoteHasExistingData =
125 List.any (Tuple.second >> Maybe.isJust) list
126 in
127 if remoteHasExistingData then
128 -- 🛰️
129 Task.succeed list
130
131 else
132 -- 🏝️ → 🛰️
133 pushLocalToRemote localConfig remoteConfig { return = list }
134 )
135 |> Task.andThen
136 (\list ->
137 -- Decode remote
138 list
139 |> List.map (\( a, b ) -> ( hypaethralBitKey a, Maybe.withDefault Json.null b ))
140 |> Json.object
141 |> User.decodeHypaethralData
142 |> Task.fromResult
143 |> Task.mapError Decode.errorToString
144 )
145 |> Task.andThen
146 (\remoteData ->
147 -- Compare modifiedAt timestamps
148 case ( remoteData.modifiedAt, localData.modifiedAt ) of
149 ( Just remoteModifiedAt, Just localModifiedAt ) ->
150 if Time.posixToMillis remoteModifiedAt == Time.posixToMillis localModifiedAt then
151 -- 🏝️
152 Task.succeed Nothing
153
154 else if Time.posixToMillis remoteModifiedAt > Time.posixToMillis localModifiedAt then
155 -- 🛰️
156 saveLocally remoteData
157
158 else
159 -- 🏝️ → 🛰️
160 pushLocalToRemote localConfig remoteConfig { return = Nothing }
161
162 ( Just _, Nothing ) ->
163 -- 🛰️
164 saveLocally remoteData
165
166 ( Nothing, Just _ ) ->
167 -- 🏝️ → 🛰️
168 pushLocalToRemote localConfig remoteConfig { return = Nothing }
169
170 _ ->
171 if noLocalData then
172 -- 🛰️
173 saveLocally remoteData
174
175 else
176 -- 🏝️
177 Task.succeed Nothing
178 )
179
180
181
182-- ㊙️
183
184
185pushLocalToRemote : LocalConfig -> RemoteConfig -> { return : a } -> Task String a
186pushLocalToRemote localConfig remoteConfig { return } =
187 localConfig.localData
188 |> User.encodedHypaethralDataList
189 |> (case localConfig.localData.modifiedAt of
190 Just localModifiedAt ->
191 (::) ( ModifiedAt, Time.encode localModifiedAt )
192
193 Nothing ->
194 identity
195 )
196 |> List.map (\( bit, data ) -> remoteConfig.save bit data)
197 |> Task.sequence
198 |> Task.map (\_ -> return)