packages feed

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 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.