mergeless 0.0.0.0 → 0.1.0.0
raw patch · 2 files changed
+223/−124 lines, 2 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
- Data.Mergeless: instance (Data.Aeson.Types.FromJSON.FromJSON a, Data.Aeson.Types.FromJSON.FromJSON i, GHC.Classes.Ord a, GHC.Classes.Ord i) => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeless.Store i a)
- Data.Mergeless: instance (Data.Aeson.Types.FromJSON.FromJSON a, GHC.Classes.Ord i, Data.Aeson.Types.FromJSON.FromJSONKey i) => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeless.CentralStore i a)
- Data.Mergeless: instance (Data.Aeson.Types.ToJSON.ToJSON a, Data.Aeson.Types.ToJSON.ToJSON i) => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeless.Store i a)
- Data.Mergeless: instance (Data.Aeson.Types.ToJSON.ToJSONKey i, Data.Aeson.Types.ToJSON.ToJSON a) => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeless.CentralStore i a)
- Data.Mergeless: instance (GHC.Classes.Eq a, GHC.Classes.Eq i) => GHC.Classes.Eq (Data.Mergeless.CentralStore i a)
- Data.Mergeless: instance (GHC.Classes.Eq a, GHC.Classes.Eq i) => GHC.Classes.Eq (Data.Mergeless.Synced i a)
- Data.Mergeless: instance (GHC.Classes.Eq i, GHC.Classes.Eq a) => GHC.Classes.Eq (Data.Mergeless.StoreItem i a)
- Data.Mergeless: instance (GHC.Classes.Eq i, GHC.Classes.Eq a) => GHC.Classes.Eq (Data.Mergeless.SyncRequest i a)
- Data.Mergeless: instance (GHC.Classes.Ord a, GHC.Classes.Ord i) => GHC.Classes.Ord (Data.Mergeless.CentralStore i a)
- Data.Mergeless: instance (GHC.Classes.Ord a, GHC.Classes.Ord i) => GHC.Classes.Ord (Data.Mergeless.Synced i a)
- Data.Mergeless: instance (GHC.Classes.Ord i, GHC.Classes.Ord a) => GHC.Classes.Ord (Data.Mergeless.StoreItem i a)
- Data.Mergeless: instance (GHC.Classes.Ord i, GHC.Classes.Ord a) => GHC.Classes.Ord (Data.Mergeless.SyncRequest i a)
- Data.Mergeless: instance (GHC.Show.Show a, GHC.Show.Show i) => GHC.Show.Show (Data.Mergeless.CentralStore i a)
- Data.Mergeless: instance (GHC.Show.Show a, GHC.Show.Show i) => GHC.Show.Show (Data.Mergeless.Synced i a)
- Data.Mergeless: instance (GHC.Show.Show i, GHC.Show.Show a) => GHC.Show.Show (Data.Mergeless.StoreItem i a)
- Data.Mergeless: instance (GHC.Show.Show i, GHC.Show.Show a) => GHC.Show.Show (Data.Mergeless.SyncRequest i a)
+ Data.Mergeless: SyncProcessor :: Set i -> m () -> Set i -> m (Set i) -> Set i -> m (Set (Synced i a)) -> Set (Synced i a) -> m () -> SyncProcessor i a m
+ Data.Mergeless: [syncProcessorDeleteMany] :: SyncProcessor i a m -> Set i -> m ()
+ Data.Mergeless: [syncProcessorInsertMany] :: SyncProcessor i a m -> Set (Synced i a) -> m ()
+ Data.Mergeless: [syncProcessorQueryNewRemote] :: SyncProcessor i a m -> Set i -> m (Set (Synced i a))
+ Data.Mergeless: [syncProcessorQuerySynced] :: SyncProcessor i a m -> Set i -> m (Set i)
+ Data.Mergeless: addItemToStore :: (Ord i, Ord a) => Added a -> Store i a -> Store i a
+ Data.Mergeless: data SyncProcessor i a m
+ Data.Mergeless: deleteSynced :: (Ord i, Ord a) => Synced i a -> Store i a -> Store i a
+ Data.Mergeless: deleteUnsynced :: (Ord i, Ord a) => Added a -> Store i a -> Store i a
+ Data.Mergeless: emptyStore :: Store i a
+ Data.Mergeless: instance (Data.Aeson.Types.FromJSON.FromJSONKey i, GHC.Classes.Ord i, Data.Aeson.Types.FromJSON.FromJSON a) => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeless.CentralStore i a)
+ Data.Mergeless: instance (Data.Aeson.Types.ToJSON.ToJSON a, Data.Aeson.Types.ToJSON.ToJSONKey i) => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeless.CentralStore i a)
+ Data.Mergeless: instance (Data.Aeson.Types.ToJSON.ToJSON i, Data.Aeson.Types.ToJSON.ToJSON a) => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeless.Store i a)
+ Data.Mergeless: instance (GHC.Classes.Eq a, GHC.Classes.Eq i) => GHC.Classes.Eq (Data.Mergeless.StoreItem i a)
+ Data.Mergeless: instance (GHC.Classes.Eq a, GHC.Classes.Eq i) => GHC.Classes.Eq (Data.Mergeless.SyncRequest i a)
+ Data.Mergeless: instance (GHC.Classes.Eq i, GHC.Classes.Eq a) => GHC.Classes.Eq (Data.Mergeless.CentralStore i a)
+ Data.Mergeless: instance (GHC.Classes.Eq i, GHC.Classes.Eq a) => GHC.Classes.Eq (Data.Mergeless.Synced i a)
+ Data.Mergeless: instance (GHC.Classes.Ord a, GHC.Classes.Ord i) => GHC.Classes.Ord (Data.Mergeless.StoreItem i a)
+ Data.Mergeless: instance (GHC.Classes.Ord a, GHC.Classes.Ord i) => GHC.Classes.Ord (Data.Mergeless.SyncRequest i a)
+ Data.Mergeless: instance (GHC.Classes.Ord a, GHC.Classes.Ord i, Data.Aeson.Types.FromJSON.FromJSON i, Data.Aeson.Types.FromJSON.FromJSON a) => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeless.Store i a)
+ Data.Mergeless: instance (GHC.Classes.Ord i, GHC.Classes.Ord a) => GHC.Classes.Ord (Data.Mergeless.CentralStore i a)
+ Data.Mergeless: instance (GHC.Classes.Ord i, GHC.Classes.Ord a) => GHC.Classes.Ord (Data.Mergeless.Synced i a)
+ Data.Mergeless: instance (GHC.Show.Show a, GHC.Show.Show i) => GHC.Show.Show (Data.Mergeless.StoreItem i a)
+ Data.Mergeless: instance (GHC.Show.Show a, GHC.Show.Show i) => GHC.Show.Show (Data.Mergeless.SyncRequest i a)
+ Data.Mergeless: instance (GHC.Show.Show i, GHC.Show.Show a) => GHC.Show.Show (Data.Mergeless.CentralStore i a)
+ Data.Mergeless: instance (GHC.Show.Show i, GHC.Show.Show a) => GHC.Show.Show (Data.Mergeless.Synced i a)
+ Data.Mergeless: instance GHC.Generics.Generic (Data.Mergeless.SyncProcessor i a m)
+ Data.Mergeless: processSyncCustom :: forall i a m. (Ord i, Ord a, Monad m) => m i -> UTCTime -> SyncProcessor i a m -> SyncRequest i a -> m (SyncResponse i a)
+ Data.Mergeless: storeSize :: Store i a -> Int
- Data.Mergeless: UndeletedItem :: i -> StoreItem i a
+ Data.Mergeless: UndeletedItem :: !i -> StoreItem i a
Files
- mergeless.cabal +2/−2
- src/Data/Mergeless.hs +221/−122
mergeless.cabal view
@@ -2,10 +2,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: 1d48986631abbde319b213203864f3a03d2b7033281b04b021493365844a0eb8+-- hash: 749159cfb0e485429b445319f97126579e191fed759cadc0903f884e813159a7 name: mergeless-version: 0.0.0.0+version: 0.1.0.0 description: Please see the README on GitHub at <https://github.com/NorfairKing/mergeless#readme> homepage: https://github.com/NorfairKing/mergeless#readme bug-reports: https://github.com/NorfairKing/mergeless/issues
src/Data/Mergeless.hs view
@@ -43,18 +43,29 @@ -- * The client sends that request to the central server and gets a 'SyncResponse'. -- * The client then updates its local store with 'mergeSyncResponse'. module Data.Mergeless- ( Store(..)- , StoreItem(..)- , Added(..)+ ( Added(..) , Synced(..)+ , StoreItem(..)+ , Store(..)+ , emptyStore+ , storeSize+ , addItemToStore+ , deleteUnsynced+ , deleteSynced , SyncRequest(..)- , makeSyncRequest , SyncResponse(..)+ -- * Client-side Synchronisation+ , makeSyncRequest , mergeSyncResponse+ -- * Server-side Synchronisation+ -- ** General synchronisation+ , SyncProcessor(..)+ , processSyncCustom+ -- ** Synchronisation with a simple central store , CentralStore(..) , CentralItem(..)- , processSync , processSyncWith+ , processSync ) where import Control.Applicative@@ -73,41 +84,7 @@ import Data.Validity.Time () import GHC.Generics (Generic) --- | A client-side store of items with Id's of type @i@ and values of type @a@-newtype Store i a = Store- { storeItems :: Set (StoreItem i a)- } deriving (Show, Eq, Ord, Generic, FromJSON, ToJSON)--instance (Validity i, Validity a, Ord i, Ord a) => Validity (Store i a) where- validate Store {..} =- mconcat- [ annotate storeItems "storeItems"- , declare "the store items have distinct uuids" $- distinct $- flip mapMaybe (S.toList storeItems) $ \case- UnsyncedItem _ -> Nothing- SyncedItem Synced {..} -> Just syncedUuid- UndeletedItem u -> Just u- ]---- | A store item with an Id of type @i@ and a value of type @a@-data StoreItem i a- = UnsyncedItem !(Added a) -- ^ A local item that has not been synchronised to the central store yet- | SyncedItem !(Synced i a) -- ^ A local item that has been synchronised to the central store already- | UndeletedItem i -- ^ An item that has been synchronised to the central store, was subsequently deleted locally but this deletion has not been synchronised to the central store yet.- deriving (Show, Eq, Ord, Generic)--instance (Validity i, Validity a) => Validity (StoreItem i a)--instance (FromJSON i, FromJSON a) => FromJSON (StoreItem i a) where- parseJSON v =- (SyncedItem <$> parseJSON v) <|> (UnsyncedItem <$> parseJSON v) <|>- (UndeletedItem <$> parseJSON v)--instance (ToJSON i, ToJSON a) => ToJSON (StoreItem i a) where- toJSON (UnsyncedItem a) = toJSON a- toJSON (SyncedItem a) = toJSON a- toJSON (UndeletedItem a) = toJSON a+{-# ANN module ("HLint: ignore Use lambda-case" :: String) #-} -- | A local item of type @a@ that has been added but not synchronised yet data Added a = Added@@ -149,6 +126,85 @@ , "synced" .= syncedSynced ] +-- | A store item with an Id of type @i@ and a value of type @a@+data StoreItem i a+ = UnsyncedItem !(Added a) -- ^ A local item that has not been synchronised to the central store yet+ | SyncedItem !(Synced i a) -- ^ A local item that has been synchronised to the central store already+ | UndeletedItem !i -- ^ An item that has been synchronised to the central store, was subsequently deleted locally but this deletion has not been synchronised to the central store yet.+ deriving (Show, Eq, Ord, Generic)++instance (Validity i, Validity a) => Validity (StoreItem i a)++instance (FromJSON i, FromJSON a) => FromJSON (StoreItem i a) where+ parseJSON v =+ (SyncedItem <$> parseJSON v) <|> (UnsyncedItem <$> parseJSON v) <|>+ (UndeletedItem <$> parseJSON v)++instance (ToJSON i, ToJSON a) => ToJSON (StoreItem i a) where+ toJSON (UnsyncedItem a) = toJSON a+ toJSON (SyncedItem a) = toJSON a+ toJSON (UndeletedItem a) = toJSON a++-- | A client-side store of items with Id's of type @i@ and values of type @a@+newtype Store i a = Store+ { storeItems :: Set (StoreItem i a)+ } deriving (Show, Eq, Ord, Generic, FromJSON, ToJSON)++instance (Validity i, Validity a, Ord i, Ord a) => Validity (Store i a) where+ validate Store {..} =+ mconcat+ [ annotate storeItems "storeItems"+ , declare "the store items have distinct uuids" $+ distinct $+ flip mapMaybe (S.toList storeItems) $ \case+ UnsyncedItem _ -> Nothing+ SyncedItem Synced {..} -> Just syncedUuid+ UndeletedItem u -> Just u+ ]++-- | The store with no items.+emptyStore :: Store i a+emptyStore = Store S.empty++-- | The number of items in a store+storeSize :: Store i a -> Int+storeSize (Store s) =+ S.size $+ S.filter+ (\si ->+ case si of+ UndeletedItem _ -> False+ _ -> True)+ s++-- | Add a new (unsynced) item to the store+addItemToStore :: (Ord i, Ord a) => Added a -> Store i a -> Store i a+addItemToStore a (Store s) = Store $ S.insert (UnsyncedItem a) s++deleteUnsynced :: (Ord i, Ord a) => Added a -> Store i a -> Store i a+deleteUnsynced a (Store s) =+ Store $+ flip mapSetMaybe s $ \si ->+ case si of+ UnsyncedItem i ->+ if i == a+ then Nothing+ else Just si+ SyncedItem _ -> Just si+ UndeletedItem _ -> Just si++deleteSynced :: (Ord i, Ord a) => Synced i a -> Store i a -> Store i a+deleteSynced a (Store s) =+ Store $+ flip mapSetMaybe s $ \si ->+ case si of+ UnsyncedItem _ -> Just si+ SyncedItem i ->+ if i == a+ then Just $ UndeletedItem $ syncedUuid i+ else Just si+ UndeletedItem _ -> Just si+ -- | A synchronisation request for items with identifiers of type @i@ and values of type @a@ data SyncRequest i a = SyncRequest { syncRequestAddedItems :: !(Set (Added a))@@ -229,19 +285,19 @@ makeSyncRequest :: (Ord i, Ord a) => Store i a -> SyncRequest i a makeSyncRequest Store {..} = SyncRequest- { syncRequestAddedItems =- flip mapSetMaybe storeItems $ \case- UnsyncedItem a -> Just a- _ -> Nothing- , syncRequestSyncedItems =- flip mapSetMaybe storeItems $ \case- SyncedItem i -> Just $ syncedUuid i- _ -> Nothing- , syncRequestUndeletedItems =- flip mapSetMaybe storeItems $ \case- UndeletedItem uuid -> Just uuid- _ -> Nothing- }+ { syncRequestAddedItems =+ flip mapSetMaybe storeItems $ \case+ UnsyncedItem a -> Just a+ _ -> Nothing+ , syncRequestSyncedItems =+ flip mapSetMaybe storeItems $ \case+ SyncedItem i -> Just $ syncedUuid i+ _ -> Nothing+ , syncRequestUndeletedItems =+ flip mapSetMaybe storeItems $ \case+ UndeletedItem uuid -> Just uuid+ _ -> Nothing+ } -- | Merge a synchronisation response back into a client-side store. mergeSyncResponse ::@@ -265,26 +321,86 @@ Nothing -> Just si -- If it wasn't deleted, don't delete it. Just _ -> Nothing -- If it was deleted, delete it here. UndeletedItem _ -> Nothing -- Delete all locally deleted items after sync- in Store- { storeItems =- S.fromList .- nubBy- (\i1 i2 ->- case (i1, i2) of- (UnsyncedItem _, _) -> False- (_, UnsyncedItem _) -> False- (SyncedItem s1, SyncedItem s2) ->- syncedUuid s1 == syncedUuid s2- (SyncedItem s1, UndeletedItem u2) ->- syncedUuid s1 == u2- (UndeletedItem u1, SyncedItem s2) ->- u1 == syncedUuid s2- (UndeletedItem u1, UndeletedItem u2) -> u1 == u2) .- S.toList $- S.map SyncedItem syncResponseNewRemoteItems `S.union`- withNewOwnItems- }+ in Store+ { storeItems =+ S.fromList .+ nubBy+ (\i1 i2 ->+ case (i1, i2) of+ (UnsyncedItem _, _) -> False+ (_, UnsyncedItem _) -> False+ (SyncedItem s1, SyncedItem s2) ->+ syncedUuid s1 == syncedUuid s2+ (SyncedItem s1, UndeletedItem u2) ->+ syncedUuid s1 == u2+ (UndeletedItem u1, SyncedItem s2) ->+ u1 == syncedUuid s2+ (UndeletedItem u1, UndeletedItem u2) -> u1 == u2) .+ S.toList $+ S.map SyncedItem syncResponseNewRemoteItems `S.union`+ withNewOwnItems+ } +-- | A record of the basic operations that are necessary to build a synchronisation processor.+data SyncProcessor i a m = SyncProcessor+ { syncProcessorDeleteMany :: Set i -> m () -- ^ Delete the items with an identifier in the given set.+ , syncProcessorQuerySynced :: Set i -> m (Set i) -- ^ Query the identifiers of the items that are in store, of the given set.+ , syncProcessorQueryNewRemote :: Set i -> m (Set (Synced i a)) -- ^ Query the items that are in store, but not in the given set.+ , syncProcessorInsertMany :: Set (Synced i a) -> m () -- ^ Insert a set of items into the stor.+ } deriving (Generic)++-- | Process a server-side synchronisation request using a custom synchronisation processor+--+-- WARNING: The identifier generation function must produce newly unique identifiers such that each new item gets a unique identifier.+--+-- You can use this function with deterministically-random identifiers or incrementing identifiers.+processSyncCustom ::+ forall i a m. (Ord i, Ord a, Monad m)+ => m i+ -> UTCTime+ -> SyncProcessor i a m+ -> SyncRequest i a+ -> m (SyncResponse i a)+processSyncCustom genUuid now SyncProcessor {..} SyncRequest {..} = do+ deleteUndeleted+ -- First we delete the items that were deleted locally but not yet remotely.+ -- Then we find the items that have been deleted remotely but not locally+ deletedRemotely <- syncItemsToBeDeletedLocally+ -- Then we find the items that have appeared remotely but aren't known locally+ newRemoteItems <- syncNewRemoteItems+ -- Then we add the items that should be added.+ newLocalItems <- syncAddedItems+ pure+ SyncResponse+ { syncResponseNewRemoteItems = newRemoteItems+ , syncResponseAddedItems = newLocalItems+ , syncResponseItemsToBeDeletedLocally = deletedRemotely+ }+ where+ deleteUndeleted :: m ()+ deleteUndeleted = syncProcessorDeleteMany syncRequestUndeletedItems+ syncItemsToBeDeletedLocally :: m (Set i)+ syncItemsToBeDeletedLocally = do+ foundItems <- syncProcessorQuerySynced syncRequestSyncedItems+ pure $ syncRequestSyncedItems `S.difference` foundItems+ syncNewRemoteItems :: m (Set (Synced i a))+ syncNewRemoteItems = syncProcessorQueryNewRemote syncRequestSyncedItems+ syncAddedItems :: m (Set (Synced i a))+ syncAddedItems = do+ is <-+ fmap S.fromList $+ forM (S.toList syncRequestAddedItems) $ \Added {..} -> do+ uuid <- genUuid+ pure+ Synced+ { syncedUuid = uuid+ , syncedCreated = addedCreated+ , syncedSynced = now+ , syncedValue = addedValue+ }+ syncProcessorInsertMany is+ pure is+ -- | An item in a central store with a value of type @a@ data CentralItem a = CentralItem { centralValue :: !a@@ -307,6 +423,8 @@ Validity (CentralStore i a) -- | Process a server-side synchronisation request using @getCurrentTime@+--+-- see 'processSyncCustom' processSync :: (Ord i, Ord a, MonadIO m) => m i@@ -319,9 +437,7 @@ -- | Process a server-side synchronisation request using a time of syncing, and an identifier generation function. ----- WARNING: The identifier generation function must produce newly unique identifiers such that each new item gets a unique identifier.------ You can use this function with deterministically-random identifiers or incrementing identifiers.+-- see 'processSyncCustom' processSyncWith :: forall i a m. (Ord i, Ord a, Monad m) => m i@@ -329,64 +445,47 @@ -> CentralStore i a -> SyncRequest i a -> m (SyncResponse i a, CentralStore i a)-processSyncWith genUuid now cs SyncRequest {..} =- flip runStateT cs $ do- deleteUndeleted- -- First we delete the items that were deleted locally but not yet remotely.- -- Then we find the items that have been deleted remotely but not locally- deletedRemotely <- syncItemsToBeDeletedLocally- -- Then we find the items that have appeared remotely but aren't known locally- newRemoteItems <- syncNewRemoteItems- -- Then we add the items that should be added.- newLocalItems <- syncAddedItems- pure- SyncResponse- { syncResponseNewRemoteItems = newRemoteItems- , syncResponseAddedItems = newLocalItems- , syncResponseItemsToBeDeletedLocally = deletedRemotely+processSyncWith genUuid now cs sr =+ flip runStateT cs $+ processSyncCustom+ (lift genUuid)+ now+ SyncProcessor+ { syncProcessorDeleteMany = deleteMany+ , syncProcessorQuerySynced = querySynced+ , syncProcessorQueryNewRemote = queryNewRemote+ , syncProcessorInsertMany = insertMany }+ sr where- deleteUndeleted :: StateT (CentralStore i a) m ()- deleteUndeleted = deleteMany syncRequestUndeletedItems deleteMany :: Set i -> StateT (CentralStore i a) m () deleteMany s = modC (`M.withoutKeys` s)- syncItemsToBeDeletedLocally :: StateT (CentralStore i a) m (Set i)- syncItemsToBeDeletedLocally = do- foundItems <- query (`M.restrictKeys` syncRequestSyncedItems)- pure $ syncRequestSyncedItems `S.difference` M.keysSet foundItems- syncNewRemoteItems :: StateT (CentralStore i a) m (Set (Synced i a))- syncNewRemoteItems = do- syncedValues <- query (`M.withoutKeys` syncRequestSyncedItems)+ querySynced :: Set i -> StateT (CentralStore i a) m (Set i)+ querySynced s = M.keysSet <$> query (`M.restrictKeys` s)+ queryNewRemote :: Set i -> StateT (CentralStore i a) m (Set (Synced i a))+ queryNewRemote s = do+ m <- query (`M.withoutKeys` s) pure $ S.fromList $- flip map (M.toList syncedValues) $ \(u, CentralItem {..}) ->+ flip map (M.toList m) $ \(i, CentralItem {..}) -> Synced- { syncedUuid = u- , syncedValue = centralValue- , syncedCreated = centralCreated- , syncedSynced = centralSynced- }+ { syncedUuid = i+ , syncedCreated = centralCreated+ , syncedSynced = centralSynced+ , syncedValue = centralValue+ } query :: (Map i (CentralItem a) -> b) -> StateT (CentralStore i a) m b query func = gets $ func . centralStoreItems- syncAddedItems :: StateT (CentralStore i a) m (Set (Synced i a))- syncAddedItems =- fmap S.fromList $- forM (S.toList syncRequestAddedItems) $ \Added {..} -> do- uuid <- lift genUuid+ insertMany :: Set (Synced i a) -> StateT (CentralStore i a) m ()+ insertMany s =+ forM_ (S.toList s) $ \Synced {..} -> ins- uuid+ syncedUuid CentralItem- { centralValue = addedValue- , centralCreated = addedCreated- , centralSynced = now- }- pure- Synced- { syncedUuid = uuid- , syncedCreated = addedCreated- , syncedSynced = now- , syncedValue = addedValue- }+ { centralValue = syncedValue+ , centralCreated = syncedCreated+ , centralSynced = syncedSynced+ } ins :: i -> CentralItem a -> StateT (CentralStore i a) m () ins i val = modC $ M.insert i val modC ::