mergeful 0.0.0.0 → 0.1.0.0
raw patch · 8 files changed
+777/−718 lines, 8 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
- Data.Mergeful: ClientId :: Word64 -> ClientId
- Data.Mergeful: ClientStore :: Map ClientId a -> Map i (Timed a) -> Map i (Timed a) -> Map i ServerTime -> ClientStore i a
- Data.Mergeful: ItemMergeStrategy :: (a -> Timed a -> Timed a) -> (Timed a -> Maybe (Timed a)) -> (a -> Maybe a) -> ItemMergeStrategy a
- Data.Mergeful: ServerStore :: Map i (Timed a) -> ServerStore i a
- Data.Mergeful: SyncRequest :: Map ClientId a -> Map i ServerTime -> Map i (Timed a) -> Map i ServerTime -> SyncRequest i a
- Data.Mergeful: SyncResponse :: Map ClientId (i, ServerTime) -> Map i ServerTime -> Set i -> Map i (Timed a) -> Map i (Timed a) -> Set i -> Map i (Timed a) -> Map i (Timed a) -> Set i -> SyncResponse i a
- Data.Mergeful: [clientStoreAddedItems] :: ClientStore i a -> Map ClientId a
- Data.Mergeful: [clientStoreDeletedItems] :: ClientStore i a -> Map i ServerTime
- Data.Mergeful: [clientStoreSyncedButChangedItems] :: ClientStore i a -> Map i (Timed a)
- Data.Mergeful: [clientStoreSyncedItems] :: ClientStore i a -> Map i (Timed a)
- Data.Mergeful: [itemMergeStrategyMergeChangeConflict] :: ItemMergeStrategy a -> a -> Timed a -> Timed a
- Data.Mergeful: [itemMergeStrategyMergeClientDeletedConflict] :: ItemMergeStrategy a -> Timed a -> Maybe (Timed a)
- Data.Mergeful: [itemMergeStrategyMergeServerDeletedConflict] :: ItemMergeStrategy a -> a -> Maybe a
- Data.Mergeful: [serverStoreItems] :: ServerStore i a -> Map i (Timed a)
- Data.Mergeful: [syncRequestDeletedItems] :: SyncRequest i a -> Map i ServerTime
- Data.Mergeful: [syncRequestKnownButChangedItems] :: SyncRequest i a -> Map i (Timed a)
- Data.Mergeful: [syncRequestKnownItems] :: SyncRequest i a -> Map i ServerTime
- Data.Mergeful: [syncRequestNewItems] :: SyncRequest i a -> Map ClientId a
- Data.Mergeful: [syncResponseClientAdded] :: SyncResponse i a -> Map ClientId (i, ServerTime)
- Data.Mergeful: [syncResponseClientChanged] :: SyncResponse i a -> Map i ServerTime
- Data.Mergeful: [syncResponseClientDeleted] :: SyncResponse i a -> Set i
- Data.Mergeful: [syncResponseConflictsClientDeleted] :: SyncResponse i a -> Map i (Timed a)
- Data.Mergeful: [syncResponseConflictsServerDeleted] :: SyncResponse i a -> Set i
- Data.Mergeful: [syncResponseConflicts] :: SyncResponse i a -> Map i (Timed a)
- Data.Mergeful: [syncResponseServerAdded] :: SyncResponse i a -> Map i (Timed a)
- Data.Mergeful: [syncResponseServerChanged] :: SyncResponse i a -> Map i (Timed a)
- Data.Mergeful: [syncResponseServerDeleted] :: SyncResponse i a -> Set i
- Data.Mergeful: [unClientId] :: ClientId -> Word64
- Data.Mergeful: addItemToClientStore :: a -> ClientStore i a -> ClientStore i a
- Data.Mergeful: addToSyncResponse :: Ord i => SyncResponse i a -> Identifier i -> ItemSyncResponse a -> SyncResponse i a
- Data.Mergeful: changeItemInClientStore :: Ord i => i -> a -> ClientStore i a -> ClientStore i a
- Data.Mergeful: data ClientStore i a
- Data.Mergeful: data ItemMergeStrategy a
- Data.Mergeful: data SyncRequest i a
- Data.Mergeful: data SyncResponse i a
- Data.Mergeful: deleteItemFromClientStore :: ClientId -> ClientStore i a -> ClientStore i a
- Data.Mergeful: emptySyncResponse :: SyncResponse i a
- Data.Mergeful: initialClientStore :: ClientStore i a
- Data.Mergeful: initialServerStore :: ServerStore i a
- Data.Mergeful: initialSyncRequest :: SyncRequest i a
- Data.Mergeful: instance (Data.Aeson.Types.FromJSON.FromJSONKey i, GHC.Classes.Ord i, Data.Aeson.Types.FromJSON.FromJSON a) => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeful.ServerStore i a)
- Data.Mergeful: instance (Data.Aeson.Types.ToJSON.ToJSON a, Data.Aeson.Types.ToJSON.ToJSONKey i) => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeful.ServerStore i a)
- Data.Mergeful: instance (Data.Aeson.Types.ToJSON.ToJSON i, Data.Aeson.Types.ToJSON.ToJSONKey i, Data.Aeson.Types.ToJSON.ToJSON a) => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeful.SyncResponse i a)
- Data.Mergeful: instance (Data.Aeson.Types.ToJSON.ToJSONKey i, Data.Aeson.Types.ToJSON.ToJSON a) => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeful.ClientStore i a)
- Data.Mergeful: instance (Data.Aeson.Types.ToJSON.ToJSONKey i, Data.Aeson.Types.ToJSON.ToJSON a) => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeful.SyncRequest i a)
- Data.Mergeful: instance (Data.Validity.Validity i, GHC.Classes.Ord i, Data.Validity.Validity a) => Data.Validity.Validity (Data.Mergeful.ClientStore i a)
- Data.Mergeful: instance (Data.Validity.Validity i, GHC.Classes.Ord i, Data.Validity.Validity a) => Data.Validity.Validity (Data.Mergeful.ServerStore i a)
- Data.Mergeful: instance (Data.Validity.Validity i, GHC.Classes.Ord i, Data.Validity.Validity a) => Data.Validity.Validity (Data.Mergeful.SyncRequest i a)
- Data.Mergeful: instance (Data.Validity.Validity i, GHC.Classes.Ord i, Data.Validity.Validity a) => Data.Validity.Validity (Data.Mergeful.SyncResponse i a)
- Data.Mergeful: instance (GHC.Classes.Eq a, GHC.Classes.Eq b) => GHC.Classes.Eq (Data.Mergeful.These a b)
- Data.Mergeful: instance (GHC.Classes.Eq a, GHC.Classes.Eq i) => GHC.Classes.Eq (Data.Mergeful.ClientStore i a)
- Data.Mergeful: instance (GHC.Classes.Eq a, GHC.Classes.Eq i) => GHC.Classes.Eq (Data.Mergeful.SyncRequest i a)
- Data.Mergeful: instance (GHC.Classes.Eq i, GHC.Classes.Eq a) => GHC.Classes.Eq (Data.Mergeful.ServerStore i a)
- Data.Mergeful: instance (GHC.Classes.Eq i, GHC.Classes.Eq a) => GHC.Classes.Eq (Data.Mergeful.SyncResponse i a)
- Data.Mergeful: instance (GHC.Classes.Ord i, Data.Aeson.Types.FromJSON.FromJSON i, Data.Aeson.Types.FromJSON.FromJSONKey i, Data.Aeson.Types.FromJSON.FromJSON a) => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeful.SyncResponse i a)
- Data.Mergeful: instance (GHC.Classes.Ord i, Data.Aeson.Types.FromJSON.FromJSONKey i, Data.Aeson.Types.FromJSON.FromJSON a) => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeful.ClientStore i a)
- Data.Mergeful: instance (GHC.Classes.Ord i, Data.Aeson.Types.FromJSON.FromJSONKey i, Data.Aeson.Types.FromJSON.FromJSON a) => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeful.SyncRequest i a)
- Data.Mergeful: instance (GHC.Show.Show a, GHC.Show.Show b) => GHC.Show.Show (Data.Mergeful.These a b)
- Data.Mergeful: instance (GHC.Show.Show a, GHC.Show.Show i) => GHC.Show.Show (Data.Mergeful.ClientStore i a)
- Data.Mergeful: instance (GHC.Show.Show a, GHC.Show.Show i) => GHC.Show.Show (Data.Mergeful.SyncRequest i a)
- Data.Mergeful: instance (GHC.Show.Show i, GHC.Show.Show a) => GHC.Show.Show (Data.Mergeful.ServerStore i a)
- Data.Mergeful: instance (GHC.Show.Show i, GHC.Show.Show a) => GHC.Show.Show (Data.Mergeful.SyncResponse i a)
- Data.Mergeful: instance Data.Aeson.Types.FromJSON.FromJSON Data.Mergeful.ClientId
- Data.Mergeful: instance Data.Aeson.Types.FromJSON.FromJSONKey Data.Mergeful.ClientId
- Data.Mergeful: instance Data.Aeson.Types.ToJSON.ToJSON Data.Mergeful.ClientId
- Data.Mergeful: instance Data.Aeson.Types.ToJSON.ToJSONKey Data.Mergeful.ClientId
- Data.Mergeful: instance Data.Validity.Validity Data.Mergeful.ClientId
- Data.Mergeful: instance Data.Validity.Validity i => Data.Validity.Validity (Data.Mergeful.Identifier i)
- Data.Mergeful: instance GHC.Classes.Eq Data.Mergeful.ClientId
- Data.Mergeful: instance GHC.Classes.Eq i => GHC.Classes.Eq (Data.Mergeful.Identifier i)
- Data.Mergeful: instance GHC.Classes.Ord Data.Mergeful.ClientId
- Data.Mergeful: instance GHC.Classes.Ord i => GHC.Classes.Ord (Data.Mergeful.Identifier i)
- Data.Mergeful: instance GHC.Enum.Bounded Data.Mergeful.ClientId
- Data.Mergeful: instance GHC.Enum.Enum Data.Mergeful.ClientId
- Data.Mergeful: instance GHC.Generics.Generic (Data.Mergeful.ClientStore i a)
- Data.Mergeful: instance GHC.Generics.Generic (Data.Mergeful.Identifier i)
- Data.Mergeful: instance GHC.Generics.Generic (Data.Mergeful.ServerStore i a)
- Data.Mergeful: instance GHC.Generics.Generic (Data.Mergeful.SyncRequest i a)
- Data.Mergeful: instance GHC.Generics.Generic (Data.Mergeful.SyncResponse i a)
- Data.Mergeful: instance GHC.Generics.Generic (Data.Mergeful.These a b)
- Data.Mergeful: instance GHC.Generics.Generic Data.Mergeful.ClientId
- Data.Mergeful: instance GHC.Show.Show Data.Mergeful.ClientId
- Data.Mergeful: instance GHC.Show.Show i => GHC.Show.Show (Data.Mergeful.Identifier i)
- Data.Mergeful: makeSyncRequest :: ClientStore i a -> SyncRequest i a
- Data.Mergeful: markItemDeletedInClientStore :: Ord i => i -> ClientStore i a -> ClientStore i a
- Data.Mergeful: mergeAddedItems :: forall i a. Ord i => Map ClientId a -> Map ClientId (i, ServerTime) -> (Map ClientId a, Map i (Timed a))
- Data.Mergeful: mergeDeletedItems :: Ord i => Map i b -> Set i -> Map i b
- Data.Mergeful: mergeSyncResponseFromServer :: Ord i => ClientStore i a -> SyncResponse i a -> ClientStore i a
- Data.Mergeful: mergeSyncResponseIgnoreProblems :: Ord i => ClientStore i a -> SyncResponse i a -> ClientStore i a
- Data.Mergeful: mergeSyncResponseUsingStrategy :: Ord i => ItemMergeStrategy a -> ClientStore i a -> SyncResponse i a -> ClientStore i a
- Data.Mergeful: mergeSyncedButChangedItems :: forall i a. Ord i => Map i (Timed a) -> Map i ServerTime -> (Map i (Timed a), Map i (Timed a))
- Data.Mergeful: newtype ClientId
- Data.Mergeful: newtype ServerStore i a
- Data.Mergeful: processServerSync :: forall i a m. (Ord i, Monad m) => m i -> ServerStore i a -> SyncRequest i a -> m (SyncResponse i a, ServerStore i a)
- Data.Mergeful.Value: data ServerValue a
+ Data.Mergeful.Collection: ClientId :: Word64 -> ClientId
+ Data.Mergeful.Collection: ClientStore :: Map ClientId a -> Map i (Timed a) -> Map i (Timed a) -> Map i ServerTime -> ClientStore i a
+ Data.Mergeful.Collection: ItemMergeStrategy :: (a -> Timed a -> Timed a) -> (Timed a -> Maybe (Timed a)) -> (a -> Maybe a) -> ItemMergeStrategy a
+ Data.Mergeful.Collection: ServerStore :: Map i (Timed a) -> ServerStore i a
+ Data.Mergeful.Collection: SyncRequest :: Map ClientId a -> Map i ServerTime -> Map i (Timed a) -> Map i ServerTime -> SyncRequest i a
+ Data.Mergeful.Collection: SyncResponse :: Map ClientId (i, ServerTime) -> Map i ServerTime -> Set i -> Map i (Timed a) -> Map i (Timed a) -> Set i -> Map i (Timed a) -> Map i (Timed a) -> Set i -> SyncResponse i a
+ Data.Mergeful.Collection: [clientStoreAddedItems] :: ClientStore i a -> Map ClientId a
+ Data.Mergeful.Collection: [clientStoreDeletedItems] :: ClientStore i a -> Map i ServerTime
+ Data.Mergeful.Collection: [clientStoreSyncedButChangedItems] :: ClientStore i a -> Map i (Timed a)
+ Data.Mergeful.Collection: [clientStoreSyncedItems] :: ClientStore i a -> Map i (Timed a)
+ Data.Mergeful.Collection: [itemMergeStrategyMergeChangeConflict] :: ItemMergeStrategy a -> a -> Timed a -> Timed a
+ Data.Mergeful.Collection: [itemMergeStrategyMergeClientDeletedConflict] :: ItemMergeStrategy a -> Timed a -> Maybe (Timed a)
+ Data.Mergeful.Collection: [itemMergeStrategyMergeServerDeletedConflict] :: ItemMergeStrategy a -> a -> Maybe a
+ Data.Mergeful.Collection: [serverStoreItems] :: ServerStore i a -> Map i (Timed a)
+ Data.Mergeful.Collection: [syncRequestDeletedItems] :: SyncRequest i a -> Map i ServerTime
+ Data.Mergeful.Collection: [syncRequestKnownButChangedItems] :: SyncRequest i a -> Map i (Timed a)
+ Data.Mergeful.Collection: [syncRequestKnownItems] :: SyncRequest i a -> Map i ServerTime
+ Data.Mergeful.Collection: [syncRequestNewItems] :: SyncRequest i a -> Map ClientId a
+ Data.Mergeful.Collection: [syncResponseClientAdded] :: SyncResponse i a -> Map ClientId (i, ServerTime)
+ Data.Mergeful.Collection: [syncResponseClientChanged] :: SyncResponse i a -> Map i ServerTime
+ Data.Mergeful.Collection: [syncResponseClientDeleted] :: SyncResponse i a -> Set i
+ Data.Mergeful.Collection: [syncResponseConflictsClientDeleted] :: SyncResponse i a -> Map i (Timed a)
+ Data.Mergeful.Collection: [syncResponseConflictsServerDeleted] :: SyncResponse i a -> Set i
+ Data.Mergeful.Collection: [syncResponseConflicts] :: SyncResponse i a -> Map i (Timed a)
+ Data.Mergeful.Collection: [syncResponseServerAdded] :: SyncResponse i a -> Map i (Timed a)
+ Data.Mergeful.Collection: [syncResponseServerChanged] :: SyncResponse i a -> Map i (Timed a)
+ Data.Mergeful.Collection: [syncResponseServerDeleted] :: SyncResponse i a -> Set i
+ Data.Mergeful.Collection: [unClientId] :: ClientId -> Word64
+ Data.Mergeful.Collection: addItemToClientStore :: a -> ClientStore i a -> ClientStore i a
+ Data.Mergeful.Collection: addToSyncResponse :: Ord i => SyncResponse i a -> Identifier i -> ItemSyncResponse a -> SyncResponse i a
+ Data.Mergeful.Collection: changeItemInClientStore :: Ord i => i -> a -> ClientStore i a -> ClientStore i a
+ Data.Mergeful.Collection: clientStoreClientIdSet :: ClientStore i a -> Set ClientId
+ Data.Mergeful.Collection: clientStoreItems :: Ord i => ClientStore i a -> Map (Either ClientId i) a
+ Data.Mergeful.Collection: clientStoreSize :: ClientStore i a -> Word
+ Data.Mergeful.Collection: clientStoreSyncIdSet :: Ord i => ClientStore i a -> Set i
+ Data.Mergeful.Collection: clientStoreUndeletedSyncIdSet :: Ord i => ClientStore i a -> Set i
+ Data.Mergeful.Collection: data ClientStore i a
+ Data.Mergeful.Collection: data ItemMergeStrategy a
+ Data.Mergeful.Collection: data SyncRequest i a
+ Data.Mergeful.Collection: data SyncResponse i a
+ Data.Mergeful.Collection: deleteItemFromClientStore :: ClientId -> ClientStore i a -> ClientStore i a
+ Data.Mergeful.Collection: emptySyncResponse :: SyncResponse i a
+ Data.Mergeful.Collection: initialClientStore :: ClientStore i a
+ Data.Mergeful.Collection: initialServerStore :: ServerStore i a
+ Data.Mergeful.Collection: initialSyncRequest :: SyncRequest i a
+ Data.Mergeful.Collection: instance (Data.Aeson.Types.FromJSON.FromJSONKey i, GHC.Classes.Ord i, Data.Aeson.Types.FromJSON.FromJSON a) => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeful.Collection.ServerStore i a)
+ Data.Mergeful.Collection: instance (Data.Aeson.Types.ToJSON.ToJSON a, Data.Aeson.Types.ToJSON.ToJSONKey i) => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeful.Collection.ServerStore i a)
+ Data.Mergeful.Collection: instance (Data.Aeson.Types.ToJSON.ToJSON i, Data.Aeson.Types.ToJSON.ToJSONKey i, Data.Aeson.Types.ToJSON.ToJSON a) => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeful.Collection.SyncResponse i a)
+ Data.Mergeful.Collection: instance (Data.Aeson.Types.ToJSON.ToJSONKey i, Data.Aeson.Types.ToJSON.ToJSON a) => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeful.Collection.ClientStore i a)
+ Data.Mergeful.Collection: instance (Data.Aeson.Types.ToJSON.ToJSONKey i, Data.Aeson.Types.ToJSON.ToJSON a) => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeful.Collection.SyncRequest i a)
+ Data.Mergeful.Collection: instance (Data.Validity.Validity i, GHC.Show.Show i, GHC.Classes.Ord i, Data.Validity.Validity a) => Data.Validity.Validity (Data.Mergeful.Collection.ClientStore i a)
+ Data.Mergeful.Collection: instance (Data.Validity.Validity i, GHC.Show.Show i, GHC.Classes.Ord i, Data.Validity.Validity a) => Data.Validity.Validity (Data.Mergeful.Collection.ServerStore i a)
+ Data.Mergeful.Collection: instance (Data.Validity.Validity i, GHC.Show.Show i, GHC.Classes.Ord i, Data.Validity.Validity a) => Data.Validity.Validity (Data.Mergeful.Collection.SyncRequest i a)
+ Data.Mergeful.Collection: instance (Data.Validity.Validity i, GHC.Show.Show i, GHC.Classes.Ord i, Data.Validity.Validity a) => Data.Validity.Validity (Data.Mergeful.Collection.SyncResponse i a)
+ Data.Mergeful.Collection: instance (GHC.Classes.Eq a, GHC.Classes.Eq b) => GHC.Classes.Eq (Data.Mergeful.Collection.These a b)
+ Data.Mergeful.Collection: instance (GHC.Classes.Eq a, GHC.Classes.Eq i) => GHC.Classes.Eq (Data.Mergeful.Collection.ClientStore i a)
+ Data.Mergeful.Collection: instance (GHC.Classes.Eq a, GHC.Classes.Eq i) => GHC.Classes.Eq (Data.Mergeful.Collection.SyncRequest i a)
+ Data.Mergeful.Collection: instance (GHC.Classes.Eq i, GHC.Classes.Eq a) => GHC.Classes.Eq (Data.Mergeful.Collection.ServerStore i a)
+ Data.Mergeful.Collection: instance (GHC.Classes.Eq i, GHC.Classes.Eq a) => GHC.Classes.Eq (Data.Mergeful.Collection.SyncResponse i a)
+ Data.Mergeful.Collection: instance (GHC.Classes.Ord i, Data.Aeson.Types.FromJSON.FromJSON i, Data.Aeson.Types.FromJSON.FromJSONKey i, Data.Aeson.Types.FromJSON.FromJSON a) => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeful.Collection.SyncResponse i a)
+ Data.Mergeful.Collection: instance (GHC.Classes.Ord i, Data.Aeson.Types.FromJSON.FromJSONKey i, Data.Aeson.Types.FromJSON.FromJSON a) => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeful.Collection.ClientStore i a)
+ Data.Mergeful.Collection: instance (GHC.Classes.Ord i, Data.Aeson.Types.FromJSON.FromJSONKey i, Data.Aeson.Types.FromJSON.FromJSON a) => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeful.Collection.SyncRequest i a)
+ Data.Mergeful.Collection: instance (GHC.Show.Show a, GHC.Show.Show b) => GHC.Show.Show (Data.Mergeful.Collection.These a b)
+ Data.Mergeful.Collection: instance (GHC.Show.Show a, GHC.Show.Show i) => GHC.Show.Show (Data.Mergeful.Collection.ClientStore i a)
+ Data.Mergeful.Collection: instance (GHC.Show.Show a, GHC.Show.Show i) => GHC.Show.Show (Data.Mergeful.Collection.SyncRequest i a)
+ Data.Mergeful.Collection: instance (GHC.Show.Show i, GHC.Show.Show a) => GHC.Show.Show (Data.Mergeful.Collection.ServerStore i a)
+ Data.Mergeful.Collection: instance (GHC.Show.Show i, GHC.Show.Show a) => GHC.Show.Show (Data.Mergeful.Collection.SyncResponse i a)
+ Data.Mergeful.Collection: instance Data.Aeson.Types.FromJSON.FromJSON Data.Mergeful.Collection.ClientId
+ Data.Mergeful.Collection: instance Data.Aeson.Types.FromJSON.FromJSONKey Data.Mergeful.Collection.ClientId
+ Data.Mergeful.Collection: instance Data.Aeson.Types.ToJSON.ToJSON Data.Mergeful.Collection.ClientId
+ Data.Mergeful.Collection: instance Data.Aeson.Types.ToJSON.ToJSONKey Data.Mergeful.Collection.ClientId
+ Data.Mergeful.Collection: instance Data.Validity.Validity Data.Mergeful.Collection.ClientId
+ Data.Mergeful.Collection: instance Data.Validity.Validity i => Data.Validity.Validity (Data.Mergeful.Collection.Identifier i)
+ Data.Mergeful.Collection: instance GHC.Classes.Eq Data.Mergeful.Collection.ClientId
+ Data.Mergeful.Collection: instance GHC.Classes.Eq i => GHC.Classes.Eq (Data.Mergeful.Collection.Identifier i)
+ Data.Mergeful.Collection: instance GHC.Classes.Ord Data.Mergeful.Collection.ClientId
+ Data.Mergeful.Collection: instance GHC.Classes.Ord i => GHC.Classes.Ord (Data.Mergeful.Collection.Identifier i)
+ Data.Mergeful.Collection: instance GHC.Enum.Bounded Data.Mergeful.Collection.ClientId
+ Data.Mergeful.Collection: instance GHC.Enum.Enum Data.Mergeful.Collection.ClientId
+ Data.Mergeful.Collection: instance GHC.Generics.Generic (Data.Mergeful.Collection.ClientStore i a)
+ Data.Mergeful.Collection: instance GHC.Generics.Generic (Data.Mergeful.Collection.Identifier i)
+ Data.Mergeful.Collection: instance GHC.Generics.Generic (Data.Mergeful.Collection.ServerStore i a)
+ Data.Mergeful.Collection: instance GHC.Generics.Generic (Data.Mergeful.Collection.SyncRequest i a)
+ Data.Mergeful.Collection: instance GHC.Generics.Generic (Data.Mergeful.Collection.SyncResponse i a)
+ Data.Mergeful.Collection: instance GHC.Generics.Generic (Data.Mergeful.Collection.These a b)
+ Data.Mergeful.Collection: instance GHC.Generics.Generic Data.Mergeful.Collection.ClientId
+ Data.Mergeful.Collection: instance GHC.Show.Show Data.Mergeful.Collection.ClientId
+ Data.Mergeful.Collection: instance GHC.Show.Show i => GHC.Show.Show (Data.Mergeful.Collection.Identifier i)
+ Data.Mergeful.Collection: makeSyncRequest :: ClientStore i a -> SyncRequest i a
+ Data.Mergeful.Collection: markItemDeletedInClientStore :: Ord i => i -> ClientStore i a -> ClientStore i a
+ Data.Mergeful.Collection: mergeAddedItems :: forall i a. Ord i => Map ClientId a -> Map ClientId (i, ServerTime) -> (Map ClientId a, Map i (Timed a))
+ Data.Mergeful.Collection: mergeDeletedItems :: Ord i => Map i b -> Set i -> Map i b
+ Data.Mergeful.Collection: mergeSyncResponseFromServer :: Ord i => ClientStore i a -> SyncResponse i a -> ClientStore i a
+ Data.Mergeful.Collection: mergeSyncResponseIgnoreProblems :: Ord i => ClientStore i a -> SyncResponse i a -> ClientStore i a
+ Data.Mergeful.Collection: mergeSyncResponseUsingStrategy :: Ord i => ItemMergeStrategy a -> ClientStore i a -> SyncResponse i a -> ClientStore i a
+ Data.Mergeful.Collection: mergeSyncedButChangedItems :: forall i a. Ord i => Map i (Timed a) -> Map i ServerTime -> (Map i (Timed a), Map i (Timed a))
+ Data.Mergeful.Collection: newtype ClientId
+ Data.Mergeful.Collection: newtype ServerStore i a
+ Data.Mergeful.Collection: processServerSync :: forall i a m. (Ord i, Monad m) => m i -> ServerStore i a -> SyncRequest i a -> m (SyncResponse i a, ServerStore i a)
+ Data.Mergeful.Value: newtype ServerValue a
- Data.Mergeful.Value: ServerValue :: !Timed a -> ServerValue a
+ Data.Mergeful.Value: ServerValue :: Timed a -> ServerValue a
Files
- CHANGELOG.md +9/−0
- ChangeLog.md +0/−3
- mergeful.cabal +5/−4
- src/Data/Mergeful.hs +2/−704
- src/Data/Mergeful/Collection.hs +757/−0
- src/Data/Mergeful/Item.hs +1/−2
- src/Data/Mergeful/Timed.hs +0/−1
- src/Data/Mergeful/Value.hs +3/−4
+ CHANGELOG.md view
@@ -0,0 +1,9 @@+# Changelog++## [Unreleased]++## [0.1.0.0] - 2019-09-23++### Changed++* Compatibility with validity-containers >=0.5
− ChangeLog.md
@@ -1,3 +0,0 @@-# Changelog for mergeful--## Unreleased changes
mergeful.cabal view
@@ -1,13 +1,13 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.31.1.+-- This file has been generated from package.yaml by hpack version 0.31.2. -- -- see: https://github.com/sol/hpack ----- hash: 1779bd15e9a78b9227b2f291e7c301be723b2ae4bb160e6f540c68cf51852fac+-- hash: 165daa0156c2d4d4295cd5dbfa2b75914c44985aca486dca68525510afa932ce name: mergeful-version: 0.0.0.0+version: 0.1.0.0 description: Please see the README on GitHub at <https://github.com/NorfairKing/mergeful#readme> homepage: https://github.com/NorfairKing/mergeful#readme bug-reports: https://github.com/NorfairKing/mergeful/issues@@ -19,7 +19,7 @@ build-type: Simple extra-source-files: README.md- ChangeLog.md+ CHANGELOG.md source-repository head type: git@@ -28,6 +28,7 @@ library exposed-modules: Data.Mergeful+ Data.Mergeful.Collection Data.Mergeful.Item Data.Mergeful.Timed Data.Mergeful.Value
src/Data/Mergeful.hs view
@@ -1,707 +1,5 @@-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE ScopedTypeVariables #-}---- | A way to synchronise a single item with safe merge conflicts.------ The setup is as follows:------ * A central server is set up to synchronise with--- * Each client synchronises with the central server, but never with eachother------------ = A client should operate as follows:------ The client starts with an 'initialClientStore'.------ * The client produces a 'SyncRequest' with 'makeSyncRequest'.--- * The client sends that request to the central server and gets a 'SyncResponse'.--- * The client then updates its local store with 'mergeSyncResponseIgnoreProblems'.--------- = The central server should operate as follows:------ The server starts with an 'initialServerStore'.------ * The server accepts a 'SyncRequest'.--- * The server performs operations according to the functionality of 'processServerSync'.--- * The server respons with a 'SyncResponse'.--------- WARNING:--- This whole approach can break down if a server resets its server times--- or if a client syncs with two different servers using the same server times. module Data.Mergeful- ( initialClientStore- , addItemToClientStore- , markItemDeletedInClientStore- , changeItemInClientStore- , deleteItemFromClientStore- , initialSyncRequest- , makeSyncRequest- , mergeSyncResponseIgnoreProblems- , mergeSyncResponseFromServer- -- * Custom merging- , ItemMergeStrategy(..)- , mergeSyncResponseUsingStrategy- -- * Server side- , initialServerStore- , processServerSync- -- * Types, for reference- , ClientStore(..)- , SyncRequest(..)- , SyncResponse(..)- , emptySyncResponse- , ServerStore(..)- , ClientId(..)- -- * Utility functions for implementing client-side merging- , mergeAddedItems- , mergeSyncedButChangedItems- , mergeDeletedItems- -- * Utility functions for implementing server-side responding- , addToSyncResponse+ ( module Data.Mergeful.Collection ) where -import GHC.Generics (Generic)--import Data.Aeson-import Data.List-import Data.Map (Map)-import qualified Data.Map as M-import Data.Maybe-import Data.Set (Set)-import qualified Data.Set as S-import Data.Text (Text)-import Data.Validity-import Data.Validity.Containers ()-import Data.Word--import Control.Applicative-import Control.Monad--import Data.Mergeful.Item-import Data.Mergeful.Timed---- | A Client-side identifier for items.------ These only need to be unique at the client.-newtype ClientId =- ClientId- { unClientId :: Word64- }- deriving (Show, Eq, Ord, Enum, Bounded, Generic, ToJSON, ToJSONKey, FromJSON, FromJSONKey)--instance Validity ClientId--data ClientStore i a =- ClientStore- { clientStoreAddedItems :: Map ClientId a- -- ^ These items are new locally but have not been synced to the server yet.- , clientStoreSyncedItems :: Map i (Timed a)- -- ^ These items have been synced at their respective 'ServerTime's.- , clientStoreSyncedButChangedItems :: Map i (Timed a)- -- ^ These items have been synced at their respective 'ServerTime's- -- but modified locally since then.- , clientStoreDeletedItems :: Map i ServerTime- -- ^ These items have been deleted locally after they were synced- -- but the server has not been notified of that yet.- }- deriving (Show, Eq, Generic)--instance (Validity i, Ord i, Validity a) => Validity (ClientStore i a) where- validate cs@ClientStore {..} =- mconcat- [ genericValidate cs- , declare "There are no duplicate IDs" $- distinct $- concat $- [ M.keys clientStoreSyncedItems- , M.keys clientStoreSyncedButChangedItems- , M.keys clientStoreDeletedItems- ]- ]--instance (Ord i, FromJSONKey i, FromJSON a) => FromJSON (ClientStore i a) where- parseJSON =- withObject "ClientStore" $ \o ->- ClientStore <$> o .:? "added" .!= M.empty <*> o .:? "synced" .!= M.empty <*>- o .:? "changed" .!= M.empty <*>- o .:? "deleted" .!= M.empty--instance (ToJSONKey i, ToJSON a) => ToJSON (ClientStore i a) where- toJSON ClientStore {..} =- object $- catMaybes $- [ jNull "added" clientStoreAddedItems- , jNull "synced" clientStoreSyncedItems- , jNull "changed" clientStoreSyncedButChangedItems- , jNull "deleted" clientStoreDeletedItems- ]---- | A client store to start with.------ This store contains no items.-initialClientStore :: ClientStore i a-initialClientStore =- ClientStore- { clientStoreAddedItems = M.empty- , clientStoreSyncedItems = M.empty- , clientStoreSyncedButChangedItems = M.empty- , clientStoreDeletedItems = M.empty- }---- | Add an item to a client store as an added item.------ This will take care of the uniqueness constraint of the 'ClientId's in the map.-addItemToClientStore :: a -> ClientStore i a -> ClientStore i a-addItemToClientStore a cs =- let oldAddedItems = clientStoreAddedItems cs- newAddedItems =- let newKey =- ClientId $- if M.null oldAddedItems- then 0- else let (ClientId k, _) = M.findMax oldAddedItems- in succ k- in M.insert newKey a oldAddedItems- in cs {clientStoreAddedItems = newAddedItems}---- | Mark an item deleted in a client store.------ This function will not delete the item, but mark it as deleted instead.-markItemDeletedInClientStore :: Ord i => i -> ClientStore i a -> ClientStore i a-markItemDeletedInClientStore u cs =- let oldSyncedItems = clientStoreSyncedItems cs- oldChangedItems = clientStoreSyncedButChangedItems cs- oldDeletedItems = clientStoreDeletedItems cs- mItem = M.lookup u oldSyncedItems <|> M.lookup u oldChangedItems- in case mItem of- Nothing -> cs- Just t ->- let newSyncedItems = M.delete u oldSyncedItems- newChangedItems = M.delete u oldChangedItems- newDeletedItems = M.insert u (timedTime t) oldDeletedItems- in cs- { clientStoreSyncedItems = newSyncedItems- , clientStoreSyncedButChangedItems = newChangedItems- , clientStoreDeletedItems = newDeletedItems- }---- | Replace the given item with a new value.------ This function will correctly mark the item as changed, if it exist.------ It will not add an item to the store with the given id, because the--- server may not have been the origin of that id.-changeItemInClientStore :: Ord i => i -> a -> ClientStore i a -> ClientStore i a-changeItemInClientStore u a cs =- case M.lookup u (clientStoreSyncedItems cs) of- Just _ ->- cs- { clientStoreSyncedItems = M.delete u (clientStoreSyncedItems cs)- , clientStoreSyncedButChangedItems =- M.adjust (\t -> t {timedValue = a}) u (clientStoreSyncedButChangedItems cs)- }- Nothing ->- case M.lookup u (clientStoreSyncedButChangedItems cs) of- Nothing -> cs- Just _ ->- cs- { clientStoreSyncedButChangedItems =- M.adjust (\t -> t {timedValue = a}) u (clientStoreSyncedButChangedItems cs)- }---- | Delete an unsynced item from a client store.------ This function will immediately delete the item, because it has never been synced.-deleteItemFromClientStore :: ClientId -> ClientStore i a -> ClientStore i a-deleteItemFromClientStore i cs = cs {clientStoreAddedItems = M.delete i (clientStoreAddedItems cs)}--newtype ServerStore i a =- ServerStore- { serverStoreItems :: Map i (Timed a)- -- ^ A map of items, named using an 'i', together with the 'ServerTime' at which- -- they were last synced.- }- deriving (Show, Eq, Generic, FromJSON, ToJSON)--instance (Validity i, Ord i, Validity a) => Validity (ServerStore i a)---- | A server store to start with------ This store contains no items.-initialServerStore :: ServerStore i a-initialServerStore = ServerStore {serverStoreItems = M.empty}--data SyncRequest i a =- SyncRequest- { syncRequestNewItems :: Map ClientId a- -- ^ These items are new locally but have not been synced to the server yet.- , syncRequestKnownItems :: Map i ServerTime- -- ^ These items have been synced at their respective 'ServerTime's.- , syncRequestKnownButChangedItems :: Map i (Timed a)- -- ^ These items have been synced at their respective 'ServerTime's- -- but modified locally since then.- , syncRequestDeletedItems :: Map i ServerTime- -- ^ These items have been deleted locally after they were synced- -- but the server has not been notified of that yet.- }- deriving (Show, Eq, Generic)--instance (Validity i, Ord i, Validity a) => Validity (SyncRequest i a) where- validate sr@SyncRequest {..} =- mconcat- [ genericValidate sr- , declare "There are no duplicate IDs" $- distinct $- concat $- [ M.keys syncRequestKnownItems- , M.keys syncRequestKnownButChangedItems- , M.keys syncRequestDeletedItems- ]- ]--instance (Ord i, FromJSONKey i, FromJSON a) => FromJSON (SyncRequest i a) where- parseJSON =- withObject "SyncRequest" $ \o ->- SyncRequest <$> o .:? "new" .!= M.empty <*> o .:? "synced" .!= M.empty <*>- o .:? "changed" .!= M.empty <*>- o .:? "deleted" .!= M.empty--instance (ToJSONKey i, ToJSON a) => ToJSON (SyncRequest i a) where- toJSON SyncRequest {..} =- object $- catMaybes $- [ jNull "new" syncRequestNewItems- , jNull "synced" syncRequestKnownItems- , jNull "changed" syncRequestKnownButChangedItems- , jNull "deleted" syncRequestDeletedItems- ]---- | An intial 'SyncRequest' to start with.------ It just asks the server to send over whatever it knows.-initialSyncRequest :: SyncRequest i a-initialSyncRequest =- SyncRequest- { syncRequestNewItems = M.empty- , syncRequestKnownItems = M.empty- , syncRequestKnownButChangedItems = M.empty- , syncRequestDeletedItems = M.empty- }--data SyncResponse i a =- SyncResponse- { syncResponseClientAdded :: Map ClientId (i, ServerTime)- -- ^ The client added these items and server has succesfully been made aware of that.- --- -- The client needs to update their server times- , syncResponseClientChanged :: Map i ServerTime- -- ^ The client changed these items and server has succesfully been made aware of that.- --- -- The client needs to update their server times- , syncResponseClientDeleted :: Set i- -- ^ The client deleted these items and server has succesfully been made aware of that.- --- -- The client can delete them from its deleted items- , syncResponseServerAdded :: Map i (Timed a)- -- ^ These items have been added on the server side- --- -- The client should add them too.- , syncResponseServerChanged :: Map i (Timed a)- -- ^ These items have been modified on the server side.- --- -- The client should modify them too.- , syncResponseServerDeleted :: Set i- -- ^ These items were deleted on the server side- --- -- The client should delete them too- , syncResponseConflicts :: Map i (Timed a)- -- ^ These are conflicts where the server and the client both have an item, but it is different.- --- -- The server kept its part of each, the client can either take whatever the server gave them- -- or deal with the conflicts somehow, and then try to re-sync.- , syncResponseConflictsClientDeleted :: Map i (Timed a)- -- ^ These are conflicts where the server has an item but the client does not.- --- -- The server kept its item, the client can either take whatever the server gave them- -- or deal with the conflicts somehow, and then try to re-sync.- , syncResponseConflictsServerDeleted :: Set i- -- ^ These are conflicts where the server has no item but the client has a modified item.- --- -- The server left its item deleted, the client can either delete its items too- -- or deal with the conflicts somehow, and then try to re-sync.- }- deriving (Show, Eq, Generic)--instance (Validity i, Ord i, Validity a) => Validity (SyncResponse i a) where- validate sr@SyncResponse {..} =- mconcat- [ genericValidate sr- , declare "There are no duplicate IDs" $- distinct $- concat $- [ map (\(_, (i, _)) -> i) $ M.toList syncResponseClientAdded- , M.keys syncResponseClientChanged- , S.toList syncResponseClientDeleted- , M.keys syncResponseServerAdded- , M.keys syncResponseServerChanged- , S.toList syncResponseServerDeleted- , M.keys syncResponseConflicts- , M.keys syncResponseConflictsClientDeleted- , S.toList syncResponseConflictsServerDeleted- ]- ]--instance (Ord i, FromJSON i, FromJSONKey i, FromJSON a) => FromJSON (SyncResponse i a) where- parseJSON =- withObject "SyncResponse" $ \o ->- SyncResponse <$> o .:? "client-added" .!= M.empty <*> o .:? "client-changed" .!= M.empty <*>- o .:? "client-deleted" .!= S.empty <*>- o .:? "server-added" .!= M.empty <*>- o .:? "server-changed" .!= M.empty <*>- o .:? "server-deleted" .!= S.empty <*>- o .:? "conflict" .!= M.empty <*>- o .:? "conflict-client-deleted" .!= M.empty <*>- o .:? "conflict-server-deleted" .!= S.empty--instance (ToJSON i, ToJSONKey i, ToJSON a) => ToJSON (SyncResponse i a) where- toJSON SyncResponse {..} =- object $- catMaybes $- [ jNull "client-added" syncResponseClientAdded- , jNull "client-changed" syncResponseClientChanged- , jNull "client-deleted" syncResponseClientDeleted- , jNull "server-added" syncResponseServerAdded- , jNull "server-changed" syncResponseServerChanged- , jNull "server-deleted" syncResponseServerDeleted- , jNull "conflict" syncResponseConflicts- , jNull "conflict-client-deleted" syncResponseConflictsClientDeleted- , jNull "conflict-server-deleted" syncResponseConflictsServerDeleted- ]--emptySyncResponse :: SyncResponse i a-emptySyncResponse =- SyncResponse- { syncResponseClientAdded = M.empty- , syncResponseClientChanged = M.empty- , syncResponseClientDeleted = S.empty- , syncResponseServerAdded = M.empty- , syncResponseServerChanged = M.empty- , syncResponseServerDeleted = S.empty- , syncResponseConflicts = M.empty- , syncResponseConflictsClientDeleted = M.empty- , syncResponseConflictsServerDeleted = S.empty- }--jNull :: (Foldable f, ToJSON (f b)) => Text -> f b -> Maybe (Text, Value)-jNull n s =- if null s- then Nothing- else Just $ n .= s---- | Produce an 'SyncRequest' from a 'ClientStore'.------ Send this to the server for synchronisation.-makeSyncRequest :: ClientStore i a -> SyncRequest i a-makeSyncRequest ClientStore {..} =- SyncRequest- { syncRequestNewItems = clientStoreAddedItems- , syncRequestKnownItems = M.map timedTime clientStoreSyncedItems- , syncRequestKnownButChangedItems = clientStoreSyncedButChangedItems- , syncRequestDeletedItems = clientStoreDeletedItems- }---- | Merge an 'SyncResponse' into the current 'ClientStore'.------ This function ignores any problems that may occur.--- In the case of a conclict, it will just not update the client item.--- The next sync request will then produce a conflict again.------ Pro: No data will be lost------ __Con: Clients will diverge when conflicts occur.__-mergeSyncResponseIgnoreProblems :: Ord i => ClientStore i a -> SyncResponse i a -> ClientStore i a-mergeSyncResponseIgnoreProblems cs SyncResponse {..} =- let (addedItemsLeftovers, newSyncedItems) =- mergeAddedItems (clientStoreAddedItems cs) syncResponseClientAdded- (syncedButNotChangedLeftovers, newModifiedItems) =- mergeSyncedButChangedItems (clientStoreSyncedButChangedItems cs) syncResponseClientChanged- deletedItemsLeftovers =- mergeDeletedItems (clientStoreDeletedItems cs) syncResponseClientDeleted- synced =- M.unions- [ newSyncedItems- , syncResponseServerAdded- , syncResponseServerChanged- , newModifiedItems- , clientStoreSyncedItems cs- ]- in ClientStore- { clientStoreAddedItems = addedItemsLeftovers- , clientStoreSyncedButChangedItems = syncedButNotChangedLeftovers `M.difference` synced- , clientStoreDeletedItems = deletedItemsLeftovers `M.difference` synced- , clientStoreSyncedItems =- synced `M.difference` (M.fromSet (const ()) syncResponseServerDeleted)- }---- | Merge an 'SyncResponse' into the current 'ClientStore' with the given merge strategy.------ In order for clients to converge on the same collection correctly, this function must be:------ * Associative--- * Idempotent--- * The same on all clients------ This function ignores mismatches.-mergeSyncResponseUsingStrategy ::- Ord i => ItemMergeStrategy a -> ClientStore i a -> SyncResponse i a -> ClientStore i a-mergeSyncResponseUsingStrategy ItemMergeStrategy {..} cs SyncResponse {..} =- let (addedItemsLeftovers, newSyncedItems) =- mergeAddedItems (clientStoreAddedItems cs) syncResponseClientAdded- (syncedButNotChangedLeftovers, newModifiedItems) =- mergeSyncedButChangedItems (clientStoreSyncedButChangedItems cs) syncResponseClientChanged- deletedItemsLeftovers =- mergeDeletedItems (clientStoreDeletedItems cs) syncResponseClientDeleted- synced =- M.unions- [ newSyncedItems- , syncResponseServerAdded- , syncResponseServerChanged- -- Merge the synced but changed (the only ones that could have caused a conflict)- -- with the ones that the response indicated were a conflict.- , M.intersectionWith- itemMergeStrategyMergeChangeConflict- (M.map timedValue $ clientStoreSyncedButChangedItems cs)- syncResponseConflicts- -- Of the items that the server changed but the client deleted,- -- keep the ones that the strategy wants to keep.- , M.mapMaybe id $- M.intersectionWith- (\_ t -> itemMergeStrategyMergeClientDeletedConflict t)- (clientStoreDeletedItems cs)- syncResponseConflictsClientDeleted- , newModifiedItems- , clientStoreSyncedItems cs- ]- -- | The synced but changed items that were not acknowledged as changed,- -- minus the ones that the strategy decided to delete.- newSyncedButChangedItems =- syncedButNotChangedLeftovers `M.difference`- M.fromSet (const ()) syncResponseConflictsServerDeleted- in ClientStore- { clientStoreAddedItems = addedItemsLeftovers- , clientStoreSyncedButChangedItems = newSyncedButChangedItems `M.difference` synced- , clientStoreDeletedItems = deletedItemsLeftovers `M.difference` synced- , clientStoreSyncedItems =- synced `M.difference` (M.fromSet (const ()) syncResponseServerDeleted)- }---- | Merge an 'SyncResponse' into the current 'ClientStore' by taking whatever the server gave the client.------ Pro: Clients will converge on the same value.------ __Con: Conflicting updates will be lost.__-mergeSyncResponseFromServer :: Ord i => ClientStore i a -> SyncResponse i a -> ClientStore i a-mergeSyncResponseFromServer =- mergeSyncResponseUsingStrategy- ItemMergeStrategy- { itemMergeStrategyMergeChangeConflict = \_ serverItem -> serverItem- , itemMergeStrategyMergeClientDeletedConflict = \serverItem -> Just serverItem- , itemMergeStrategyMergeServerDeletedConflict = \_ -> Nothing- }---- | Merge the local added items with the ones that the server has acknowledged as added.-mergeAddedItems ::- forall i a. Ord i- => Map ClientId a- -> Map ClientId (i, ServerTime)- -> (Map ClientId a, Map i (Timed a))-mergeAddedItems local added = M.foldlWithKey go (M.empty, M.empty) local- where- go :: (Map ClientId a, Map i (Timed a)) -> ClientId -> a -> (Map ClientId a, Map i (Timed a))- go (as, m) i a =- case M.lookup i added of- Nothing -> (M.insert i a as, m)- Just (k, st) -> (as, M.insert k (Timed {timedValue = a, timedTime = st}) m)---- | Merge the local synced but changed items with the ones that the server has acknowledged as changed.-mergeSyncedButChangedItems ::- forall i a. Ord i- => Map i (Timed a)- -> Map i ServerTime- -> (Map i (Timed a), Map i (Timed a))-mergeSyncedButChangedItems local changed = M.foldlWithKey go (M.empty, M.empty) local- where- go :: (Map i (Timed a), Map i (Timed a)) -> i -> Timed a -> (Map i (Timed a), Map i (Timed a))- go (m1, m2) k t =- case M.lookup k changed of- Nothing -> (M.insert k t m1, m2)- Just st' -> (m1, M.insert k (t {timedTime = st'}) m2)---- | Merge the local deleted items with the ones that the server has acknowledged as deleted.-mergeDeletedItems :: Ord i => Map i b -> Set i -> (Map i b)-mergeDeletedItems m s = m `M.difference` M.fromSet (const ()) s--data Identifier i- = OnlyServer i- | BothServerAndClient i ClientId- deriving (Show, Eq, Ord, Generic)--instance Validity i => Validity (Identifier i)---- | Serve an 'SyncRequest' using the current 'ServerStore', producing an 'SyncResponse' and a new 'ServerStore'.-processServerSync ::- forall i a m. (Ord i, Monad m)- => m i -- ^ The action that is guaranteed to generate unique identifiers- -> ServerStore i a- -> SyncRequest i a- -> m (SyncResponse i a, ServerStore i a)-processServerSync genId ServerStore {..} sr@SyncRequest {..}- -- Make tuples of requests for all of the items that only had a client identifier.- = do- let unidentifedPairs :: Map ClientId (ServerItem a, ItemSyncRequest a)- unidentifedPairs = M.map (\a -> (ServerEmpty, ItemSyncRequestNew a)) syncRequestNewItems- -- Make tuples of results for each of the unidentifier tuples.- unidentifedResults :: Map ClientId (ItemSyncResponse a, ServerItem a)- unidentifedResults = M.map (uncurry processServerItemSync) unidentifedPairs- generatedResults <- generateIdentifiersFor genId unidentifedResults- -- Gather the items that had a server identifier already.- let clientIdentifiedSyncRequests :: Map i (ItemSyncRequest a)- clientIdentifiedSyncRequests = identifiedItemSyncRequests sr- -- Make 'ServerItem's for each of the items on the server side- serverIdentifiedItems :: Map i (ServerItem a)- serverIdentifiedItems = M.map ServerFull serverStoreItems- -- Match up client items with server items by their id.- thesePairs :: Map i (These (ServerItem a) (ItemSyncRequest a))- thesePairs = unionTheseMaps serverIdentifiedItems clientIdentifiedSyncRequests- -- Make tuples of server 'ServerItem's and 'ItemSyncRequest's for each of the items with an id- requestPairs :: Map i (ServerItem a, ItemSyncRequest a)- requestPairs = M.map (fromThese ServerEmpty ItemSyncRequestPoll) thesePairs- -- Make tuples of results for each of the tuplus that had a server identifier.- identifiedResults :: Map i (ItemSyncResponse a, ServerItem a)- identifiedResults = M.map (uncurry processServerItemSync) requestPairs- -- Put together the results together- let allResults :: Map (Identifier i) (ItemSyncResponse a, ServerItem a)- allResults =- M.union- (M.mapKeys OnlyServer identifiedResults)- (M.mapKeys (uncurry BothServerAndClient) generatedResults)- pure $ produceSyncResults allResults--identifiedItemSyncRequests :: Ord i => SyncRequest i a -> Map i (ItemSyncRequest a)-identifiedItemSyncRequests SyncRequest {..} =- M.unions- [ M.map ItemSyncRequestKnown syncRequestKnownItems- , M.map ItemSyncRequestKnownButChanged syncRequestKnownButChangedItems- , M.map ItemSyncRequestDeletedLocally syncRequestDeletedItems- ]--generateIdentifiersFor ::- (Ord i, Monad m)- => m i- -> Map ClientId (ItemSyncResponse a, ServerItem a)- -> m (Map (i, ClientId) (ItemSyncResponse a, ServerItem a))-generateIdentifiersFor genId unidentifedResults =- fmap M.fromList $- forM (M.toList unidentifedResults) $ \(int, r) -> do- uuid <- genId- pure ((uuid, int), r)--produceSyncResults ::- forall i a. Ord i- => Map (Identifier i) (ItemSyncResponse a, ServerItem a)- -> (SyncResponse i a, ServerStore i a)-produceSyncResults allResults- -- Produce a sync response- =- let resp :: SyncResponse i a- resp =- M.foldlWithKey- (\sr cid (isr, _) -> addToSyncResponse sr cid isr)- emptySyncResponse- allResults- -- Produce a new server store- newStore :: Map i (Timed a)- newStore =- M.mapMaybe- (\si ->- case si of- ServerEmpty -> Nothing- ServerFull t -> Just t) $- M.map snd $- M.mapKeys- (\cid ->- case cid of- OnlyServer i -> i- BothServerAndClient i _ -> i)- allResults- -- return them both.- in (resp, ServerStore newStore)---- | Given an incomplete 'SyncResponse', an id, possibly a client ID too, and--- an 'ItemSyncResponse', produce a less incomplete 'SyncResponse'.-addToSyncResponse ::- Ord i => SyncResponse i a -> Identifier i -> ItemSyncResponse a -> SyncResponse i a-addToSyncResponse sr cid isr =- case cid of- BothServerAndClient i int ->- case isr of- ItemSyncResponseClientAdded st ->- sr {syncResponseClientAdded = M.insert int (i, st) $ syncResponseClientAdded sr}- _ -> error "should not happen"- OnlyServer i ->- case isr of- ItemSyncResponseInSyncEmpty -> sr- ItemSyncResponseInSyncFull -> sr- ItemSyncResponseClientAdded _ -> sr -- Should not happen.- ItemSyncResponseClientChanged st ->- sr {syncResponseClientChanged = M.insert i st $ syncResponseClientChanged sr}- ItemSyncResponseClientDeleted ->- sr {syncResponseClientDeleted = S.insert i $ syncResponseClientDeleted sr}- ItemSyncResponseServerAdded t ->- sr {syncResponseServerAdded = M.insert i t $ syncResponseServerAdded sr}- ItemSyncResponseServerChanged t ->- sr {syncResponseServerChanged = M.insert i t $ syncResponseServerChanged sr}- ItemSyncResponseServerDeleted ->- sr {syncResponseServerDeleted = S.insert i $ syncResponseServerDeleted sr}- ItemSyncResponseConflict a ->- sr {syncResponseConflicts = M.insert i a $ syncResponseConflicts sr}- ItemSyncResponseConflictClientDeleted a ->- sr- { syncResponseConflictsClientDeleted =- M.insert i a $ syncResponseConflictsClientDeleted sr- }- ItemSyncResponseConflictServerDeleted ->- sr- { syncResponseConflictsServerDeleted =- S.insert i $ syncResponseConflictsServerDeleted sr- }--unionTheseMaps :: Ord k => Map k a -> Map k b -> Map k (These a b)-unionTheseMaps m1 m2 = M.unionWith go (M.map This m1) (M.map That m2)- where- go (This a) (That b) = These a b- go _ _ = error "should not happen."--distinct :: Eq a => [a] -> Bool-distinct ls = nub ls == ls---- Inlined because holy smokes, `these` has a _lot_ of dependencies.-data These a b- = This a- | That b- | These a b- deriving (Show, Eq, Generic)--fromThese :: a -> b -> These a b -> (a, b)-fromThese a b t =- case t of- This a' -> (a', b)- That b' -> (a, b')- These a' b' -> (a', b')+import Data.Mergeful.Collection
+ src/Data/Mergeful/Collection.hs view
@@ -0,0 +1,757 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}++-- | A way to synchronise a single item with safe merge conflicts.+--+-- The setup is as follows:+--+-- * A central server is set up to synchronise with+-- * Each client synchronises with the central server, but never with eachother+--+--+--+-- = A client should operate as follows:+--+-- The client starts with an 'initialClientStore'.+--+-- * The client produces a 'SyncRequest' with 'makeSyncRequest'.+-- * The client sends that request to the central server and gets a 'SyncResponse'.+-- * The client then updates its local store with 'mergeSyncResponseIgnoreProblems'.+--+--+-- = The central server should operate as follows:+--+-- The server starts with an 'initialServerStore'.+--+-- * The server accepts a 'SyncRequest'.+-- * The server performs operations according to the functionality of 'processServerSync'.+-- * The server respons with a 'SyncResponse'.+--+--+-- WARNING:+-- This whole approach can break down if a server resets its server times+-- or if a client syncs with two different servers using the same server times.+module Data.Mergeful.Collection+ ( initialClientStore+ , clientStoreSize+ , clientStoreClientIdSet+ , clientStoreUndeletedSyncIdSet+ , clientStoreSyncIdSet+ , clientStoreItems+ , addItemToClientStore+ , markItemDeletedInClientStore+ , changeItemInClientStore+ , deleteItemFromClientStore+ , initialSyncRequest+ , makeSyncRequest+ , mergeSyncResponseIgnoreProblems+ , mergeSyncResponseFromServer+ -- * Custom merging+ , ItemMergeStrategy(..)+ , mergeSyncResponseUsingStrategy+ -- * Server side+ , initialServerStore+ , processServerSync+ -- * Types, for reference+ , ClientStore(..)+ , SyncRequest(..)+ , SyncResponse(..)+ , emptySyncResponse+ , ServerStore(..)+ , ClientId(..)+ -- * Utility functions for implementing client-side merging+ , mergeAddedItems+ , mergeSyncedButChangedItems+ , mergeDeletedItems+ -- * Utility functions for implementing server-side responding+ , addToSyncResponse+ ) where++import GHC.Generics (Generic)++import Data.Aeson+import Data.List+import Data.Map (Map)+import qualified Data.Map as M+import Data.Maybe+import Data.Set (Set)+import qualified Data.Set as S+import Data.Text (Text)+import Data.Validity+import Data.Validity.Containers ()+import Data.Word++import Control.Applicative+import Control.Monad++import Data.Mergeful.Item+import Data.Mergeful.Timed++-- | A Client-side identifier for items.+--+-- These only need to be unique at the client.+newtype ClientId =+ ClientId+ { unClientId :: Word64+ }+ deriving (Show, Eq, Ord, Enum, Bounded, Generic, ToJSON, ToJSONKey, FromJSON, FromJSONKey)++instance Validity ClientId++data ClientStore i a =+ ClientStore+ { clientStoreAddedItems :: Map ClientId a+ -- ^ These items are new locally but have not been synced to the server yet.+ , clientStoreSyncedItems :: Map i (Timed a)+ -- ^ These items have been synced at their respective 'ServerTime's.+ , clientStoreSyncedButChangedItems :: Map i (Timed a)+ -- ^ These items have been synced at their respective 'ServerTime's+ -- but modified locally since then.+ , clientStoreDeletedItems :: Map i ServerTime+ -- ^ These items have been deleted locally after they were synced+ -- but the server has not been notified of that yet.+ }+ deriving (Show, Eq, Generic)++instance (Validity i, Show i, Ord i, Validity a) => Validity (ClientStore i a) where+ validate cs@ClientStore {..} =+ mconcat+ [ genericValidate cs+ , declare "There are no duplicate IDs" $+ distinct $+ concat+ [ M.keys clientStoreSyncedItems+ , M.keys clientStoreSyncedButChangedItems+ , M.keys clientStoreDeletedItems+ ]+ ]++instance (Ord i, FromJSONKey i, FromJSON a) => FromJSON (ClientStore i a) where+ parseJSON =+ withObject "ClientStore" $ \o ->+ ClientStore <$> o .:? "added" .!= M.empty <*> o .:? "synced" .!= M.empty <*>+ o .:? "changed" .!= M.empty <*>+ o .:? "deleted" .!= M.empty++instance (ToJSONKey i, ToJSON a) => ToJSON (ClientStore i a) where+ toJSON ClientStore {..} =+ object $+ catMaybes+ [ jNull "added" clientStoreAddedItems+ , jNull "synced" clientStoreSyncedItems+ , jNull "changed" clientStoreSyncedButChangedItems+ , jNull "deleted" clientStoreDeletedItems+ ]++-- | A client store to start with.+--+-- This store contains no items.+initialClientStore :: ClientStore i a+initialClientStore =+ ClientStore+ { clientStoreAddedItems = M.empty+ , clientStoreSyncedItems = M.empty+ , clientStoreSyncedButChangedItems = M.empty+ , clientStoreDeletedItems = M.empty+ }++-- | The number of items in a client store+--+-- This does not count the deleted items, so that they really look deleted..+clientStoreSize :: ClientStore i a -> Word+clientStoreSize ClientStore {..} =+ fromIntegral $+ sum+ [ M.size clientStoreAddedItems+ , M.size clientStoreSyncedItems+ , M.size clientStoreSyncedButChangedItems+ ]++-- | The set of client ids.+--+-- These are only the client ids of the added items that have not been synced yet.+clientStoreClientIdSet :: ClientStore i a -> Set ClientId+clientStoreClientIdSet ClientStore {..} = M.keysSet clientStoreAddedItems++-- | The set of server ids.+--+-- This does not include the ids of items that have been marked as deleted.+clientStoreUndeletedSyncIdSet :: Ord i => ClientStore i a -> Set i+clientStoreUndeletedSyncIdSet ClientStore {..} =+ S.unions [M.keysSet clientStoreSyncedItems, M.keysSet clientStoreSyncedButChangedItems]++-- | The set of server ids.+--+-- This includes the ids of items that have been marked as deleted.+clientStoreSyncIdSet :: Ord i => ClientStore i a -> Set i+clientStoreSyncIdSet ClientStore {..} =+ S.unions+ [ M.keysSet clientStoreSyncedItems+ , M.keysSet clientStoreSyncedButChangedItems+ , M.keysSet clientStoreDeletedItems+ ]++-- | The set of items in the client store+--+-- This map does not include items that have been marked as deleted.+clientStoreItems :: Ord i => ClientStore i a -> Map (Either ClientId i) a+clientStoreItems ClientStore {..} =+ M.unions+ [ M.mapKeys Left clientStoreAddedItems+ , M.mapKeys Right $ M.map timedValue clientStoreSyncedItems+ , M.mapKeys Right $ M.map timedValue clientStoreSyncedButChangedItems+ ]++-- | Add an item to a client store as an added item.+--+-- This will take care of the uniqueness constraint of the 'ClientId's in the map.+addItemToClientStore :: a -> ClientStore i a -> ClientStore i a+addItemToClientStore a cs =+ let oldAddedItems = clientStoreAddedItems cs+ newAddedItems =+ let newKey =+ ClientId $+ if M.null oldAddedItems+ then 0+ else let (ClientId k, _) = M.findMax oldAddedItems+ in succ k+ in M.insert newKey a oldAddedItems+ in cs {clientStoreAddedItems = newAddedItems}++-- | Mark an item deleted in a client store.+--+-- This function will not delete the item, but mark it as deleted instead.+markItemDeletedInClientStore :: Ord i => i -> ClientStore i a -> ClientStore i a+markItemDeletedInClientStore u cs =+ let oldSyncedItems = clientStoreSyncedItems cs+ oldChangedItems = clientStoreSyncedButChangedItems cs+ oldDeletedItems = clientStoreDeletedItems cs+ mItem = M.lookup u oldSyncedItems <|> M.lookup u oldChangedItems+ in case mItem of+ Nothing -> cs+ Just t ->+ let newSyncedItems = M.delete u oldSyncedItems+ newChangedItems = M.delete u oldChangedItems+ newDeletedItems = M.insert u (timedTime t) oldDeletedItems+ in cs+ { clientStoreSyncedItems = newSyncedItems+ , clientStoreSyncedButChangedItems = newChangedItems+ , clientStoreDeletedItems = newDeletedItems+ }++-- | Replace the given item with a new value.+--+-- This function will correctly mark the item as changed, if it exist.+--+-- It will not add an item to the store with the given id, because the+-- server may not have been the origin of that id.+changeItemInClientStore :: Ord i => i -> a -> ClientStore i a -> ClientStore i a+changeItemInClientStore i a cs =+ case M.lookup i (clientStoreSyncedItems cs) of+ Just t ->+ cs+ { clientStoreSyncedItems = M.delete i (clientStoreSyncedItems cs)+ , clientStoreSyncedButChangedItems =+ M.insert i (t {timedValue = a}) (clientStoreSyncedButChangedItems cs)+ }+ Nothing ->+ case M.lookup i (clientStoreSyncedButChangedItems cs) of+ Nothing -> cs+ Just _ ->+ cs+ { clientStoreSyncedButChangedItems =+ M.adjust (\t -> t {timedValue = a}) i (clientStoreSyncedButChangedItems cs)+ }++-- | Delete an unsynced item from a client store.+--+-- This function will immediately delete the item, because it has never been synced.+deleteItemFromClientStore :: ClientId -> ClientStore i a -> ClientStore i a+deleteItemFromClientStore i cs = cs {clientStoreAddedItems = M.delete i (clientStoreAddedItems cs)}++newtype ServerStore i a =+ ServerStore+ { serverStoreItems :: Map i (Timed a)+ -- ^ A map of items, named using an 'i', together with the 'ServerTime' at which+ -- they were last synced.+ }+ deriving (Show, Eq, Generic, FromJSON, ToJSON)++instance (Validity i, Show i, Ord i, Validity a) => Validity (ServerStore i a)++-- | A server store to start with+--+-- This store contains no items.+initialServerStore :: ServerStore i a+initialServerStore = ServerStore {serverStoreItems = M.empty}++data SyncRequest i a =+ SyncRequest+ { syncRequestNewItems :: Map ClientId a+ -- ^ These items are new locally but have not been synced to the server yet.+ , syncRequestKnownItems :: Map i ServerTime+ -- ^ These items have been synced at their respective 'ServerTime's.+ , syncRequestKnownButChangedItems :: Map i (Timed a)+ -- ^ These items have been synced at their respective 'ServerTime's+ -- but modified locally since then.+ , syncRequestDeletedItems :: Map i ServerTime+ -- ^ These items have been deleted locally after they were synced+ -- but the server has not been notified of that yet.+ }+ deriving (Show, Eq, Generic)++instance (Validity i, Show i, Ord i, Validity a) => Validity (SyncRequest i a) where+ validate sr@SyncRequest {..} =+ mconcat+ [ genericValidate sr+ , declare "There are no duplicate IDs" $+ distinct $+ concat+ [ M.keys syncRequestKnownItems+ , M.keys syncRequestKnownButChangedItems+ , M.keys syncRequestDeletedItems+ ]+ ]++instance (Ord i, FromJSONKey i, FromJSON a) => FromJSON (SyncRequest i a) where+ parseJSON =+ withObject "SyncRequest" $ \o ->+ SyncRequest <$> o .:? "new" .!= M.empty <*> o .:? "synced" .!= M.empty <*>+ o .:? "changed" .!= M.empty <*>+ o .:? "deleted" .!= M.empty++instance (ToJSONKey i, ToJSON a) => ToJSON (SyncRequest i a) where+ toJSON SyncRequest {..} =+ object $+ catMaybes+ [ jNull "new" syncRequestNewItems+ , jNull "synced" syncRequestKnownItems+ , jNull "changed" syncRequestKnownButChangedItems+ , jNull "deleted" syncRequestDeletedItems+ ]++-- | An intial 'SyncRequest' to start with.+--+-- It just asks the server to send over whatever it knows.+initialSyncRequest :: SyncRequest i a+initialSyncRequest =+ SyncRequest+ { syncRequestNewItems = M.empty+ , syncRequestKnownItems = M.empty+ , syncRequestKnownButChangedItems = M.empty+ , syncRequestDeletedItems = M.empty+ }++data SyncResponse i a =+ SyncResponse+ { syncResponseClientAdded :: Map ClientId (i, ServerTime)+ -- ^ The client added these items and server has succesfully been made aware of that.+ --+ -- The client needs to update their server times+ , syncResponseClientChanged :: Map i ServerTime+ -- ^ The client changed these items and server has succesfully been made aware of that.+ --+ -- The client needs to update their server times+ , syncResponseClientDeleted :: Set i+ -- ^ The client deleted these items and server has succesfully been made aware of that.+ --+ -- The client can delete them from its deleted items+ , syncResponseServerAdded :: Map i (Timed a)+ -- ^ These items have been added on the server side+ --+ -- The client should add them too.+ , syncResponseServerChanged :: Map i (Timed a)+ -- ^ These items have been modified on the server side.+ --+ -- The client should modify them too.+ , syncResponseServerDeleted :: Set i+ -- ^ These items were deleted on the server side+ --+ -- The client should delete them too+ , syncResponseConflicts :: Map i (Timed a)+ -- ^ These are conflicts where the server and the client both have an item, but it is different.+ --+ -- The server kept its part of each, the client can either take whatever the server gave them+ -- or deal with the conflicts somehow, and then try to re-sync.+ , syncResponseConflictsClientDeleted :: Map i (Timed a)+ -- ^ These are conflicts where the server has an item but the client does not.+ --+ -- The server kept its item, the client can either take whatever the server gave them+ -- or deal with the conflicts somehow, and then try to re-sync.+ , syncResponseConflictsServerDeleted :: Set i+ -- ^ These are conflicts where the server has no item but the client has a modified item.+ --+ -- The server left its item deleted, the client can either delete its items too+ -- or deal with the conflicts somehow, and then try to re-sync.+ }+ deriving (Show, Eq, Generic)++instance (Validity i, Show i, Ord i, Validity a) => Validity (SyncResponse i a) where+ validate sr@SyncResponse {..} =+ mconcat+ [ genericValidate sr+ , declare "There are no duplicate IDs" $+ distinct $+ concat+ [ map (\(_, (i, _)) -> i) $ M.toList syncResponseClientAdded+ , M.keys syncResponseClientChanged+ , S.toList syncResponseClientDeleted+ , M.keys syncResponseServerAdded+ , M.keys syncResponseServerChanged+ , S.toList syncResponseServerDeleted+ , M.keys syncResponseConflicts+ , M.keys syncResponseConflictsClientDeleted+ , S.toList syncResponseConflictsServerDeleted+ ]+ ]++instance (Ord i, FromJSON i, FromJSONKey i, FromJSON a) => FromJSON (SyncResponse i a) where+ parseJSON =+ withObject "SyncResponse" $ \o ->+ SyncResponse <$> o .:? "client-added" .!= M.empty <*> o .:? "client-changed" .!= M.empty <*>+ o .:? "client-deleted" .!= S.empty <*>+ o .:? "server-added" .!= M.empty <*>+ o .:? "server-changed" .!= M.empty <*>+ o .:? "server-deleted" .!= S.empty <*>+ o .:? "conflict" .!= M.empty <*>+ o .:? "conflict-client-deleted" .!= M.empty <*>+ o .:? "conflict-server-deleted" .!= S.empty++instance (ToJSON i, ToJSONKey i, ToJSON a) => ToJSON (SyncResponse i a) where+ toJSON SyncResponse {..} =+ object $+ catMaybes+ [ jNull "client-added" syncResponseClientAdded+ , jNull "client-changed" syncResponseClientChanged+ , jNull "client-deleted" syncResponseClientDeleted+ , jNull "server-added" syncResponseServerAdded+ , jNull "server-changed" syncResponseServerChanged+ , jNull "server-deleted" syncResponseServerDeleted+ , jNull "conflict" syncResponseConflicts+ , jNull "conflict-client-deleted" syncResponseConflictsClientDeleted+ , jNull "conflict-server-deleted" syncResponseConflictsServerDeleted+ ]++emptySyncResponse :: SyncResponse i a+emptySyncResponse =+ SyncResponse+ { syncResponseClientAdded = M.empty+ , syncResponseClientChanged = M.empty+ , syncResponseClientDeleted = S.empty+ , syncResponseServerAdded = M.empty+ , syncResponseServerChanged = M.empty+ , syncResponseServerDeleted = S.empty+ , syncResponseConflicts = M.empty+ , syncResponseConflictsClientDeleted = M.empty+ , syncResponseConflictsServerDeleted = S.empty+ }++jNull :: (Foldable f, ToJSON (f b)) => Text -> f b -> Maybe (Text, Value)+jNull n s =+ if null s+ then Nothing+ else Just $ n .= s++-- | Produce an 'SyncRequest' from a 'ClientStore'.+--+-- Send this to the server for synchronisation.+makeSyncRequest :: ClientStore i a -> SyncRequest i a+makeSyncRequest ClientStore {..} =+ SyncRequest+ { syncRequestNewItems = clientStoreAddedItems+ , syncRequestKnownItems = M.map timedTime clientStoreSyncedItems+ , syncRequestKnownButChangedItems = clientStoreSyncedButChangedItems+ , syncRequestDeletedItems = clientStoreDeletedItems+ }++-- | Merge an 'SyncResponse' into the current 'ClientStore'.+--+-- This function ignores any problems that may occur.+-- In the case of a conclict, it will just not update the client item.+-- The next sync request will then produce a conflict again.+--+-- Pro: No data will be lost+--+-- __Con: Clients will diverge when conflicts occur.__+mergeSyncResponseIgnoreProblems :: Ord i => ClientStore i a -> SyncResponse i a -> ClientStore i a+mergeSyncResponseIgnoreProblems cs SyncResponse {..} =+ let (addedItemsLeftovers, newSyncedItems) =+ mergeAddedItems (clientStoreAddedItems cs) syncResponseClientAdded+ (syncedButNotChangedLeftovers, newModifiedItems) =+ mergeSyncedButChangedItems (clientStoreSyncedButChangedItems cs) syncResponseClientChanged+ deletedItemsLeftovers =+ mergeDeletedItems (clientStoreDeletedItems cs) syncResponseClientDeleted+ synced =+ M.unions+ [ newSyncedItems+ , syncResponseServerAdded+ , syncResponseServerChanged+ , newModifiedItems+ , clientStoreSyncedItems cs+ ]+ in ClientStore+ { clientStoreAddedItems = addedItemsLeftovers+ , clientStoreSyncedButChangedItems = syncedButNotChangedLeftovers `M.difference` synced+ , clientStoreDeletedItems = deletedItemsLeftovers `M.difference` synced+ , clientStoreSyncedItems =+ synced `M.difference` M.fromSet (const ()) syncResponseServerDeleted+ }++-- | Merge an 'SyncResponse' into the current 'ClientStore' with the given merge strategy.+--+-- In order for clients to converge on the same collection correctly, this function must be:+--+-- * Associative+-- * Idempotent+-- * The same on all clients+--+-- This function ignores mismatches.+mergeSyncResponseUsingStrategy ::+ Ord i => ItemMergeStrategy a -> ClientStore i a -> SyncResponse i a -> ClientStore i a+mergeSyncResponseUsingStrategy ItemMergeStrategy {..} cs SyncResponse {..} =+ let (addedItemsLeftovers, newSyncedItems) =+ mergeAddedItems (clientStoreAddedItems cs) syncResponseClientAdded+ (syncedButNotChangedLeftovers, newModifiedItems) =+ mergeSyncedButChangedItems (clientStoreSyncedButChangedItems cs) syncResponseClientChanged+ deletedItemsLeftovers =+ mergeDeletedItems (clientStoreDeletedItems cs) syncResponseClientDeleted+ synced =+ M.unions+ [ newSyncedItems+ , syncResponseServerAdded+ , syncResponseServerChanged+ -- Merge the synced but changed (the only ones that could have caused a conflict)+ -- with the ones that the response indicated were a conflict.+ , M.intersectionWith+ itemMergeStrategyMergeChangeConflict+ (M.map timedValue $ clientStoreSyncedButChangedItems cs)+ syncResponseConflicts+ -- Of the items that the server changed but the client deleted,+ -- keep the ones that the strategy wants to keep.+ , M.mapMaybe id $+ M.intersectionWith+ (\_ t -> itemMergeStrategyMergeClientDeletedConflict t)+ (clientStoreDeletedItems cs)+ syncResponseConflictsClientDeleted+ , newModifiedItems+ , clientStoreSyncedItems cs+ ]+ -- | The synced but changed items that were not acknowledged as changed,+ -- minus the ones that the strategy decided to delete.+ newSyncedButChangedItems =+ syncedButNotChangedLeftovers `M.difference`+ M.fromSet (const ()) syncResponseConflictsServerDeleted+ in ClientStore+ { clientStoreAddedItems = addedItemsLeftovers+ , clientStoreSyncedButChangedItems = newSyncedButChangedItems `M.difference` synced+ , clientStoreDeletedItems = deletedItemsLeftovers `M.difference` synced+ , clientStoreSyncedItems =+ synced `M.difference` M.fromSet (const ()) syncResponseServerDeleted+ }++-- | Merge an 'SyncResponse' into the current 'ClientStore' by taking whatever the server gave the client.+--+-- Pro: Clients will converge on the same value.+--+-- __Con: Conflicting updates will be lost.__+mergeSyncResponseFromServer :: Ord i => ClientStore i a -> SyncResponse i a -> ClientStore i a+mergeSyncResponseFromServer =+ mergeSyncResponseUsingStrategy+ ItemMergeStrategy+ { itemMergeStrategyMergeChangeConflict = \_ serverItem -> serverItem+ , itemMergeStrategyMergeClientDeletedConflict = \serverItem -> Just serverItem+ , itemMergeStrategyMergeServerDeletedConflict = \_ -> Nothing+ }++-- | Merge the local added items with the ones that the server has acknowledged as added.+mergeAddedItems ::+ forall i a. Ord i+ => Map ClientId a+ -> Map ClientId (i, ServerTime)+ -> (Map ClientId a, Map i (Timed a))+mergeAddedItems local added = M.foldlWithKey go (M.empty, M.empty) local+ where+ go :: (Map ClientId a, Map i (Timed a)) -> ClientId -> a -> (Map ClientId a, Map i (Timed a))+ go (as, m) i a =+ case M.lookup i added of+ Nothing -> (M.insert i a as, m)+ Just (k, st) -> (as, M.insert k (Timed {timedValue = a, timedTime = st}) m)++-- | Merge the local synced but changed items with the ones that the server has acknowledged as changed.+mergeSyncedButChangedItems ::+ forall i a. Ord i+ => Map i (Timed a)+ -> Map i ServerTime+ -> (Map i (Timed a), Map i (Timed a))+mergeSyncedButChangedItems local changed = M.foldlWithKey go (M.empty, M.empty) local+ where+ go :: (Map i (Timed a), Map i (Timed a)) -> i -> Timed a -> (Map i (Timed a), Map i (Timed a))+ go (m1, m2) k t =+ case M.lookup k changed of+ Nothing -> (M.insert k t m1, m2)+ Just st' -> (m1, M.insert k (t {timedTime = st'}) m2)++-- | Merge the local deleted items with the ones that the server has acknowledged as deleted.+mergeDeletedItems :: Ord i => Map i b -> Set i -> Map i b+mergeDeletedItems m s = m `M.difference` M.fromSet (const ()) s++data Identifier i+ = OnlyServer i+ | BothServerAndClient i ClientId+ deriving (Show, Eq, Ord, Generic)++instance Validity i => Validity (Identifier i)++-- | Serve an 'SyncRequest' using the current 'ServerStore', producing an 'SyncResponse' and a new 'ServerStore'.+processServerSync ::+ forall i a m. (Ord i, Monad m)+ => m i -- ^ The action that is guaranteed to generate unique identifiers+ -> ServerStore i a+ -> SyncRequest i a+ -> m (SyncResponse i a, ServerStore i a)+processServerSync genId ServerStore {..} sr@SyncRequest {..}+ -- Make tuples of requests for all of the items that only had a client identifier.+ = do+ let unidentifedPairs :: Map ClientId (ServerItem a, ItemSyncRequest a)+ unidentifedPairs = M.map (\a -> (ServerEmpty, ItemSyncRequestNew a)) syncRequestNewItems+ -- Make tuples of results for each of the unidentifier tuples.+ unidentifedResults :: Map ClientId (ItemSyncResponse a, ServerItem a)+ unidentifedResults = M.map (uncurry processServerItemSync) unidentifedPairs+ generatedResults <- generateIdentifiersFor genId unidentifedResults+ -- Gather the items that had a server identifier already.+ let clientIdentifiedSyncRequests :: Map i (ItemSyncRequest a)+ clientIdentifiedSyncRequests = identifiedItemSyncRequests sr+ -- Make 'ServerItem's for each of the items on the server side+ serverIdentifiedItems :: Map i (ServerItem a)+ serverIdentifiedItems = M.map ServerFull serverStoreItems+ -- Match up client items with server items by their id.+ thesePairs :: Map i (These (ServerItem a) (ItemSyncRequest a))+ thesePairs = unionTheseMaps serverIdentifiedItems clientIdentifiedSyncRequests+ -- Make tuples of server 'ServerItem's and 'ItemSyncRequest's for each of the items with an id+ requestPairs :: Map i (ServerItem a, ItemSyncRequest a)+ requestPairs = M.map (fromThese ServerEmpty ItemSyncRequestPoll) thesePairs+ -- Make tuples of results for each of the tuplus that had a server identifier.+ identifiedResults :: Map i (ItemSyncResponse a, ServerItem a)+ identifiedResults = M.map (uncurry processServerItemSync) requestPairs+ -- Put together the results together+ let allResults :: Map (Identifier i) (ItemSyncResponse a, ServerItem a)+ allResults =+ M.union+ (M.mapKeys OnlyServer identifiedResults)+ (M.mapKeys (uncurry BothServerAndClient) generatedResults)+ pure $ produceSyncResults allResults++identifiedItemSyncRequests :: Ord i => SyncRequest i a -> Map i (ItemSyncRequest a)+identifiedItemSyncRequests SyncRequest {..} =+ M.unions+ [ M.map ItemSyncRequestKnown syncRequestKnownItems+ , M.map ItemSyncRequestKnownButChanged syncRequestKnownButChangedItems+ , M.map ItemSyncRequestDeletedLocally syncRequestDeletedItems+ ]++generateIdentifiersFor ::+ (Ord i, Monad m)+ => m i+ -> Map ClientId (ItemSyncResponse a, ServerItem a)+ -> m (Map (i, ClientId) (ItemSyncResponse a, ServerItem a))+generateIdentifiersFor genId unidentifedResults =+ fmap M.fromList $+ forM (M.toList unidentifedResults) $ \(int, r) -> do+ uuid <- genId+ pure ((uuid, int), r)++produceSyncResults ::+ forall i a. Ord i+ => Map (Identifier i) (ItemSyncResponse a, ServerItem a)+ -> (SyncResponse i a, ServerStore i a)+produceSyncResults allResults+ -- Produce a sync response+ =+ let resp :: SyncResponse i a+ resp =+ M.foldlWithKey+ (\sr cid (isr, _) -> addToSyncResponse sr cid isr)+ emptySyncResponse+ allResults+ -- Produce a new server store+ newStore :: Map i (Timed a)+ newStore =+ M.mapMaybe+ (\case+ ServerEmpty -> Nothing+ ServerFull t -> Just t) $+ M.map snd $+ M.mapKeys+ (\case+ OnlyServer i -> i+ BothServerAndClient i _ -> i)+ allResults+ -- return them both.+ in (resp, ServerStore newStore)++-- | Given an incomplete 'SyncResponse', an id, possibly a client ID too, and+-- an 'ItemSyncResponse', produce a less incomplete 'SyncResponse'.+addToSyncResponse ::+ Ord i => SyncResponse i a -> Identifier i -> ItemSyncResponse a -> SyncResponse i a+addToSyncResponse sr cid isr =+ case cid of+ BothServerAndClient i int ->+ case isr of+ ItemSyncResponseClientAdded st ->+ sr {syncResponseClientAdded = M.insert int (i, st) $ syncResponseClientAdded sr}+ _ -> error "should not happen"+ OnlyServer i ->+ case isr of+ ItemSyncResponseInSyncEmpty -> sr+ ItemSyncResponseInSyncFull -> sr+ ItemSyncResponseClientAdded _ -> sr -- Should not happen.+ ItemSyncResponseClientChanged st ->+ sr {syncResponseClientChanged = M.insert i st $ syncResponseClientChanged sr}+ ItemSyncResponseClientDeleted ->+ sr {syncResponseClientDeleted = S.insert i $ syncResponseClientDeleted sr}+ ItemSyncResponseServerAdded t ->+ sr {syncResponseServerAdded = M.insert i t $ syncResponseServerAdded sr}+ ItemSyncResponseServerChanged t ->+ sr {syncResponseServerChanged = M.insert i t $ syncResponseServerChanged sr}+ ItemSyncResponseServerDeleted ->+ sr {syncResponseServerDeleted = S.insert i $ syncResponseServerDeleted sr}+ ItemSyncResponseConflict a ->+ sr {syncResponseConflicts = M.insert i a $ syncResponseConflicts sr}+ ItemSyncResponseConflictClientDeleted a ->+ sr+ { syncResponseConflictsClientDeleted =+ M.insert i a $ syncResponseConflictsClientDeleted sr+ }+ ItemSyncResponseConflictServerDeleted ->+ sr+ { syncResponseConflictsServerDeleted =+ S.insert i $ syncResponseConflictsServerDeleted sr+ }++unionTheseMaps :: Ord k => Map k a -> Map k b -> Map k (These a b)+unionTheseMaps m1 m2 = M.unionWith go (M.map This m1) (M.map That m2)+ where+ go (This a) (That b) = These a b+ go _ _ = error "should not happen."++distinct :: Eq a => [a] -> Bool+distinct ls = nub ls == ls++-- Inlined because holy smokes, `these` has a _lot_ of dependencies.+data These a b+ = This a+ | That b+ | These a b+ deriving (Show, Eq, Generic)++fromThese :: a -> b -> These a b -> (a, b)+fromThese a b t =+ case t of+ This a' -> (a', b)+ That b' -> (a, b')+ These a' b' -> (a', b')
src/Data/Mergeful/Item.hs view
@@ -1,6 +1,5 @@ {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} @@ -490,7 +489,7 @@ -- No conflict here. else ( ItemSyncResponseServerChanged (Timed {timedValue = si, timedTime = st}) , store)- ItemSyncRequestKnownButChanged (Timed {timedValue = ci, timedTime = ct}) ->+ ItemSyncRequestKnownButChanged Timed {timedValue = ci, timedTime = ct} -> if ct >= st -- The client time is equal to the server time. -- The client indicates that the item *was* modified at their side.
src/Data/Mergeful/Timed.hs view
@@ -1,4 +1,3 @@-{-# LANGUAGE BangPatterns #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}
src/Data/Mergeful/Value.hs view
@@ -1,6 +1,5 @@ {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} @@ -114,8 +113,8 @@ -- -- The only difference between 'a' and 'ServerValue a' is that 'ServerValue a' also -- remembers the last time this value was changed during synchronisation.-data ServerValue a =- ServerValue !(Timed a)+newtype ServerValue a =+ ServerValue (Timed a) deriving (Show, Eq, Generic) instance Validity a => Validity (ServerValue a)@@ -322,7 +321,7 @@ -- we can just send it back to the client to have them update their version. -- No conflict here. else (ValueSyncResponseServerChanged t, sv)- ValueSyncRequestKnownButChanged (Timed {timedValue = ci, timedTime = ct}) ->+ ValueSyncRequestKnownButChanged Timed {timedValue = ci, timedTime = ct} -> if ct >= st -- The client time is equal to the server time. -- The client indicates that the item *was* modified at their side.