mergeless 0.1.0.0 → 0.2.0.0
raw patch · 5 files changed
+599/−505 lines, 5 filesdep +deepseqdep −timedep −validity-timePVP ok
version bump matches the API change (PVP)
Dependencies added: deepseq
Dependencies removed: time, validity-time
API changes (from Hackage documentation)
- Data.Mergeless: Added :: !a -> !UTCTime -> Added a
- Data.Mergeless: CentralItem :: !a -> !UTCTime -> !UTCTime -> CentralItem a
- Data.Mergeless: CentralStore :: Map i (CentralItem a) -> CentralStore i a
- Data.Mergeless: Store :: Set (StoreItem i a) -> Store 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: SyncRequest :: !(Set (Added a)) -> !(Set i) -> !(Set i) -> SyncRequest i a
- Data.Mergeless: SyncResponse :: !(Set (Synced i a)) -> !(Set (Synced i a)) -> !(Set i) -> SyncResponse i a
- Data.Mergeless: Synced :: i -> !a -> !UTCTime -> !UTCTime -> Synced i a
- Data.Mergeless: SyncedItem :: !(Synced i a) -> StoreItem i a
- Data.Mergeless: UndeletedItem :: !i -> StoreItem i a
- Data.Mergeless: UnsyncedItem :: !(Added a) -> StoreItem i a
- Data.Mergeless: [addedCreated] :: Added a -> !UTCTime
- Data.Mergeless: [addedValue] :: Added a -> !a
- Data.Mergeless: [centralCreated] :: CentralItem a -> !UTCTime
- Data.Mergeless: [centralStoreItems] :: CentralStore i a -> Map i (CentralItem a)
- Data.Mergeless: [centralSynced] :: CentralItem a -> !UTCTime
- Data.Mergeless: [centralValue] :: CentralItem a -> !a
- Data.Mergeless: [storeItems] :: Store i a -> Set (StoreItem i a)
- 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: [syncRequestAddedItems] :: SyncRequest i a -> !(Set (Added a))
- Data.Mergeless: [syncRequestSyncedItems] :: SyncRequest i a -> !(Set i)
- Data.Mergeless: [syncRequestUndeletedItems] :: SyncRequest i a -> !(Set i)
- Data.Mergeless: [syncResponseAddedItems] :: SyncResponse i a -> !(Set (Synced i a))
- Data.Mergeless: [syncResponseItemsToBeDeletedLocally] :: SyncResponse i a -> !(Set i)
- Data.Mergeless: [syncResponseNewRemoteItems] :: SyncResponse i a -> !(Set (Synced i a))
- Data.Mergeless: [syncedCreated] :: Synced i a -> !UTCTime
- Data.Mergeless: [syncedSynced] :: Synced i a -> !UTCTime
- Data.Mergeless: [syncedUuid] :: Synced i a -> i
- Data.Mergeless: [syncedValue] :: Synced i a -> !a
- Data.Mergeless: addItemToStore :: (Ord i, Ord a) => Added a -> Store i a -> Store i a
- Data.Mergeless: data Added a
- Data.Mergeless: data CentralItem a
- Data.Mergeless: data StoreItem i a
- Data.Mergeless: data SyncProcessor i a m
- Data.Mergeless: data SyncRequest i a
- Data.Mergeless: data SyncResponse i a
- Data.Mergeless: data Synced i a
- 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.FromJSON i, Data.Aeson.Types.FromJSON.FromJSON a) => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeless.StoreItem i a)
- Data.Mergeless: instance (Data.Aeson.Types.FromJSON.FromJSON i, Data.Aeson.Types.FromJSON.FromJSON a) => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeless.Synced i a)
- Data.Mergeless: instance (Data.Aeson.Types.FromJSON.FromJSON i, Data.Aeson.Types.FromJSON.FromJSON a, GHC.Classes.Ord i, GHC.Classes.Ord a) => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeless.SyncRequest i a)
- Data.Mergeless: instance (Data.Aeson.Types.FromJSON.FromJSON i, Data.Aeson.Types.FromJSON.FromJSON a, GHC.Classes.Ord i, GHC.Classes.Ord a) => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeless.SyncResponse 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 (Data.Aeson.Types.ToJSON.ToJSON i, Data.Aeson.Types.ToJSON.ToJSON a) => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeless.StoreItem i a)
- Data.Mergeless: instance (Data.Aeson.Types.ToJSON.ToJSON i, Data.Aeson.Types.ToJSON.ToJSON a) => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeless.SyncRequest i a)
- Data.Mergeless: instance (Data.Aeson.Types.ToJSON.ToJSON i, Data.Aeson.Types.ToJSON.ToJSON a) => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeless.SyncResponse i a)
- Data.Mergeless: instance (Data.Aeson.Types.ToJSON.ToJSON i, Data.Aeson.Types.ToJSON.ToJSON a) => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeless.Synced i a)
- Data.Mergeless: instance (Data.Validity.Validity i, Data.Validity.Validity a) => Data.Validity.Validity (Data.Mergeless.StoreItem i a)
- Data.Mergeless: instance (Data.Validity.Validity i, Data.Validity.Validity a) => Data.Validity.Validity (Data.Mergeless.Synced i a)
- Data.Mergeless: instance (Data.Validity.Validity i, Data.Validity.Validity a, GHC.Classes.Ord i, GHC.Classes.Ord a) => Data.Validity.Validity (Data.Mergeless.CentralStore i a)
- Data.Mergeless: instance (Data.Validity.Validity i, Data.Validity.Validity a, GHC.Classes.Ord i, GHC.Classes.Ord a) => Data.Validity.Validity (Data.Mergeless.Store i a)
- Data.Mergeless: instance (Data.Validity.Validity i, Data.Validity.Validity a, GHC.Classes.Ord i, GHC.Classes.Ord a) => Data.Validity.Validity (Data.Mergeless.SyncRequest i a)
- Data.Mergeless: instance (Data.Validity.Validity i, Data.Validity.Validity a, GHC.Classes.Ord i, GHC.Classes.Ord a) => Data.Validity.Validity (Data.Mergeless.SyncResponse i a)
- Data.Mergeless: instance (GHC.Classes.Eq a, GHC.Classes.Eq i) => GHC.Classes.Eq (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.SyncResponse 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.Store 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.SyncResponse 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.Store 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.SyncResponse i a)
- Data.Mergeless: instance (GHC.Show.Show i, GHC.Show.Show a) => GHC.Show.Show (Data.Mergeless.Synced i a)
- Data.Mergeless: instance Data.Aeson.Types.FromJSON.FromJSON a => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeless.Added a)
- Data.Mergeless: instance Data.Aeson.Types.FromJSON.FromJSON a => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeless.CentralItem a)
- Data.Mergeless: instance Data.Aeson.Types.ToJSON.ToJSON a => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeless.Added a)
- Data.Mergeless: instance Data.Aeson.Types.ToJSON.ToJSON a => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeless.CentralItem a)
- Data.Mergeless: instance Data.Validity.Validity a => Data.Validity.Validity (Data.Mergeless.Added a)
- Data.Mergeless: instance Data.Validity.Validity a => Data.Validity.Validity (Data.Mergeless.CentralItem a)
- Data.Mergeless: instance GHC.Classes.Eq a => GHC.Classes.Eq (Data.Mergeless.Added a)
- Data.Mergeless: instance GHC.Classes.Eq a => GHC.Classes.Eq (Data.Mergeless.CentralItem a)
- Data.Mergeless: instance GHC.Classes.Ord a => GHC.Classes.Ord (Data.Mergeless.Added a)
- Data.Mergeless: instance GHC.Classes.Ord a => GHC.Classes.Ord (Data.Mergeless.CentralItem a)
- Data.Mergeless: instance GHC.Generics.Generic (Data.Mergeless.Added a)
- Data.Mergeless: instance GHC.Generics.Generic (Data.Mergeless.CentralItem a)
- Data.Mergeless: instance GHC.Generics.Generic (Data.Mergeless.CentralStore i a)
- Data.Mergeless: instance GHC.Generics.Generic (Data.Mergeless.Store i a)
- Data.Mergeless: instance GHC.Generics.Generic (Data.Mergeless.StoreItem i a)
- Data.Mergeless: instance GHC.Generics.Generic (Data.Mergeless.SyncProcessor i a m)
- Data.Mergeless: instance GHC.Generics.Generic (Data.Mergeless.SyncRequest i a)
- Data.Mergeless: instance GHC.Generics.Generic (Data.Mergeless.SyncResponse i a)
- Data.Mergeless: instance GHC.Generics.Generic (Data.Mergeless.Synced i a)
- Data.Mergeless: instance GHC.Show.Show a => GHC.Show.Show (Data.Mergeless.Added a)
- Data.Mergeless: instance GHC.Show.Show a => GHC.Show.Show (Data.Mergeless.CentralItem a)
- Data.Mergeless: makeSyncRequest :: (Ord i, Ord a) => Store i a -> SyncRequest i a
- Data.Mergeless: mergeSyncResponse :: (Ord i, Ord a) => Store i a -> SyncResponse i a -> Store i a
- Data.Mergeless: newtype CentralStore i a
- Data.Mergeless: newtype Store i a
- Data.Mergeless: processSync :: (Ord i, Ord a, MonadIO m) => m i -> CentralStore i a -> SyncRequest i a -> m (SyncResponse i a, CentralStore i a)
- 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: processSyncWith :: forall i a m. (Ord i, Ord a, Monad m) => m i -> UTCTime -> CentralStore i a -> SyncRequest i a -> m (SyncResponse i a, CentralStore i a)
- Data.Mergeless: storeSize :: Store i a -> Int
+ Data.Mergeless.Collection: ClientId :: Word64 -> ClientId
+ Data.Mergeless.Collection: ClientStore :: !Map ClientId a -> !Map i a -> !Set i -> ClientStore i a
+ Data.Mergeless.Collection: ServerStore :: Map i a -> ServerStore i a
+ Data.Mergeless.Collection: ServerSyncProcessor :: (Set i -> m (Set i)) -> (Set i -> m (Set i)) -> (Set i -> m (Map i a)) -> (Map ClientId a -> m (Map ClientId i)) -> ServerSyncProcessor i a m
+ Data.Mergeless.Collection: SyncRequest :: !Map ClientId a -> !Set i -> !Set i -> SyncRequest i a
+ Data.Mergeless.Collection: SyncResponse :: !Map ClientId i -> !Set i -> !Map i a -> !Set i -> SyncResponse i a
+ Data.Mergeless.Collection: [clientStoreAdded] :: ClientStore i a -> !Map ClientId a
+ Data.Mergeless.Collection: [clientStoreDeleted] :: ClientStore i a -> !Set i
+ Data.Mergeless.Collection: [clientStoreSynced] :: ClientStore i a -> !Map i a
+ Data.Mergeless.Collection: [serverStoreItems] :: ServerStore i a -> Map i a
+ Data.Mergeless.Collection: [serverSyncProcessorDeleteMany] :: ServerSyncProcessor i a m -> Set i -> m (Set i)
+ Data.Mergeless.Collection: [serverSyncProcessorInsertMany] :: ServerSyncProcessor i a m -> Map ClientId a -> m (Map ClientId i)
+ Data.Mergeless.Collection: [serverSyncProcessorQueryNewRemote] :: ServerSyncProcessor i a m -> Set i -> m (Map i a)
+ Data.Mergeless.Collection: [serverSyncProcessorQueryNoLongerSynced] :: ServerSyncProcessor i a m -> Set i -> m (Set i)
+ Data.Mergeless.Collection: [syncRequestAdded] :: SyncRequest i a -> !Map ClientId a
+ Data.Mergeless.Collection: [syncRequestDeleted] :: SyncRequest i a -> !Set i
+ Data.Mergeless.Collection: [syncRequestSynced] :: SyncRequest i a -> !Set i
+ Data.Mergeless.Collection: [syncResponseClientAdded] :: SyncResponse i a -> !Map ClientId i
+ Data.Mergeless.Collection: [syncResponseClientDeleted] :: SyncResponse i a -> !Set i
+ Data.Mergeless.Collection: [syncResponseServerAdded] :: SyncResponse i a -> !Map i a
+ Data.Mergeless.Collection: [syncResponseServerDeleted] :: SyncResponse i a -> !Set i
+ Data.Mergeless.Collection: [unClientId] :: ClientId -> Word64
+ Data.Mergeless.Collection: addAddedItems :: forall i a. (Ord i, Ord a) => Map ClientId i -> ClientStore i a -> ClientStore i a
+ Data.Mergeless.Collection: addItemToClientStore :: (Ord i, Ord a) => a -> ClientStore i a -> ClientStore i a
+ Data.Mergeless.Collection: addRemotelyAddedItems :: (Ord i, Ord a) => Map i a -> ClientStore i a -> ClientStore i a
+ Data.Mergeless.Collection: data ClientStore i a
+ Data.Mergeless.Collection: data ServerSyncProcessor i a m
+ Data.Mergeless.Collection: data SyncRequest i a
+ Data.Mergeless.Collection: data SyncResponse i a
+ Data.Mergeless.Collection: deleteItemsToBeDeletedLocally :: (Ord i, Ord a) => Set i -> ClientStore i a -> ClientStore i a
+ Data.Mergeless.Collection: deleteLocalUndeletedItems :: (Ord i, Ord a) => Set i -> ClientStore i a -> ClientStore i a
+ Data.Mergeless.Collection: deleteSyncedFromClientStore :: (Ord i, Ord a) => i -> ClientStore i a -> ClientStore i a
+ Data.Mergeless.Collection: deleteUnsyncedFromClientStore :: (Ord i, Ord a) => ClientId -> ClientStore i a -> ClientStore i a
+ Data.Mergeless.Collection: emptyClientStore :: ClientStore i a
+ Data.Mergeless.Collection: emptyServerStore :: ServerStore i a
+ Data.Mergeless.Collection: emptySyncResponse :: SyncResponse i ia
+ Data.Mergeless.Collection: instance (Control.DeepSeq.NFData i, Control.DeepSeq.NFData a) => Control.DeepSeq.NFData (Data.Mergeless.Collection.ClientStore i a)
+ Data.Mergeless.Collection: instance (Control.DeepSeq.NFData i, Control.DeepSeq.NFData a) => Control.DeepSeq.NFData (Data.Mergeless.Collection.ServerStore i a)
+ Data.Mergeless.Collection: instance (Control.DeepSeq.NFData i, Control.DeepSeq.NFData a) => Control.DeepSeq.NFData (Data.Mergeless.Collection.SyncRequest i a)
+ Data.Mergeless.Collection: instance (Control.DeepSeq.NFData i, Control.DeepSeq.NFData a) => Control.DeepSeq.NFData (Data.Mergeless.Collection.SyncResponse i a)
+ Data.Mergeless.Collection: instance (Data.Aeson.Types.FromJSON.FromJSON i, Data.Aeson.Types.FromJSON.FromJSON a, GHC.Classes.Ord i, GHC.Classes.Ord a) => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeless.Collection.SyncRequest i a)
+ Data.Mergeless.Collection: instance (Data.Aeson.Types.FromJSON.FromJSONKey i, GHC.Classes.Ord i, Data.Aeson.Types.FromJSON.FromJSON a) => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeless.Collection.ServerStore i a)
+ Data.Mergeless.Collection: instance (Data.Aeson.Types.ToJSON.ToJSON a, Data.Aeson.Types.ToJSON.ToJSONKey i) => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeless.Collection.ServerStore i a)
+ Data.Mergeless.Collection: instance (Data.Aeson.Types.ToJSON.ToJSON i, Data.Aeson.Types.ToJSON.ToJSON a) => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeless.Collection.SyncRequest i a)
+ Data.Mergeless.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.Mergeless.Collection.SyncResponse i a)
+ Data.Mergeless.Collection: instance (Data.Validity.Validity i, Data.Validity.Validity a, GHC.Classes.Ord i, GHC.Classes.Ord a) => Data.Validity.Validity (Data.Mergeless.Collection.SyncRequest i a)
+ Data.Mergeless.Collection: instance (Data.Validity.Validity i, Data.Validity.Validity a, GHC.Show.Show i, GHC.Show.Show a, GHC.Classes.Ord i, GHC.Classes.Ord a) => Data.Validity.Validity (Data.Mergeless.Collection.ClientStore i a)
+ Data.Mergeless.Collection: instance (Data.Validity.Validity i, Data.Validity.Validity a, GHC.Show.Show i, GHC.Show.Show a, GHC.Classes.Ord i, GHC.Classes.Ord a) => Data.Validity.Validity (Data.Mergeless.Collection.ServerStore i a)
+ Data.Mergeless.Collection: instance (Data.Validity.Validity i, Data.Validity.Validity a, GHC.Show.Show i, GHC.Show.Show a, GHC.Classes.Ord i, GHC.Classes.Ord a) => Data.Validity.Validity (Data.Mergeless.Collection.SyncResponse i a)
+ Data.Mergeless.Collection: instance (GHC.Classes.Eq a, GHC.Classes.Eq i) => GHC.Classes.Eq (Data.Mergeless.Collection.ClientStore i a)
+ Data.Mergeless.Collection: instance (GHC.Classes.Eq a, GHC.Classes.Eq i) => GHC.Classes.Eq (Data.Mergeless.Collection.SyncRequest i a)
+ Data.Mergeless.Collection: instance (GHC.Classes.Eq i, GHC.Classes.Eq a) => GHC.Classes.Eq (Data.Mergeless.Collection.ServerStore i a)
+ Data.Mergeless.Collection: instance (GHC.Classes.Eq i, GHC.Classes.Eq a) => GHC.Classes.Eq (Data.Mergeless.Collection.SyncResponse i a)
+ Data.Mergeless.Collection: instance (GHC.Classes.Ord a, GHC.Classes.Ord i) => GHC.Classes.Ord (Data.Mergeless.Collection.ClientStore i a)
+ Data.Mergeless.Collection: instance (GHC.Classes.Ord a, GHC.Classes.Ord i) => GHC.Classes.Ord (Data.Mergeless.Collection.SyncRequest i a)
+ Data.Mergeless.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.Mergeless.Collection.ClientStore i a)
+ Data.Mergeless.Collection: instance (GHC.Classes.Ord i, Data.Aeson.Types.FromJSON.FromJSON i, Data.Aeson.Types.FromJSON.FromJSONKey i, GHC.Classes.Ord a, Data.Aeson.Types.FromJSON.FromJSON a) => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeless.Collection.SyncResponse i a)
+ Data.Mergeless.Collection: instance (GHC.Classes.Ord i, Data.Aeson.Types.ToJSON.ToJSON i, Data.Aeson.Types.ToJSON.ToJSONKey i, Data.Aeson.Types.ToJSON.ToJSON a) => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeless.Collection.ClientStore i a)
+ Data.Mergeless.Collection: instance (GHC.Classes.Ord i, GHC.Classes.Ord a) => GHC.Classes.Ord (Data.Mergeless.Collection.ServerStore i a)
+ Data.Mergeless.Collection: instance (GHC.Classes.Ord i, GHC.Classes.Ord a) => GHC.Classes.Ord (Data.Mergeless.Collection.SyncResponse i a)
+ Data.Mergeless.Collection: instance (GHC.Show.Show a, GHC.Show.Show i) => GHC.Show.Show (Data.Mergeless.Collection.ClientStore i a)
+ Data.Mergeless.Collection: instance (GHC.Show.Show a, GHC.Show.Show i) => GHC.Show.Show (Data.Mergeless.Collection.SyncRequest i a)
+ Data.Mergeless.Collection: instance (GHC.Show.Show i, GHC.Show.Show a) => GHC.Show.Show (Data.Mergeless.Collection.ServerStore i a)
+ Data.Mergeless.Collection: instance (GHC.Show.Show i, GHC.Show.Show a) => GHC.Show.Show (Data.Mergeless.Collection.SyncResponse i a)
+ Data.Mergeless.Collection: instance Control.DeepSeq.NFData Data.Mergeless.Collection.ClientId
+ Data.Mergeless.Collection: instance Data.Aeson.Types.FromJSON.FromJSON Data.Mergeless.Collection.ClientId
+ Data.Mergeless.Collection: instance Data.Aeson.Types.FromJSON.FromJSONKey Data.Mergeless.Collection.ClientId
+ Data.Mergeless.Collection: instance Data.Aeson.Types.ToJSON.ToJSON Data.Mergeless.Collection.ClientId
+ Data.Mergeless.Collection: instance Data.Aeson.Types.ToJSON.ToJSONKey Data.Mergeless.Collection.ClientId
+ Data.Mergeless.Collection: instance Data.Validity.Validity Data.Mergeless.Collection.ClientId
+ Data.Mergeless.Collection: instance GHC.Classes.Eq Data.Mergeless.Collection.ClientId
+ Data.Mergeless.Collection: instance GHC.Classes.Ord Data.Mergeless.Collection.ClientId
+ Data.Mergeless.Collection: instance GHC.Enum.Bounded Data.Mergeless.Collection.ClientId
+ Data.Mergeless.Collection: instance GHC.Enum.Enum Data.Mergeless.Collection.ClientId
+ Data.Mergeless.Collection: instance GHC.Generics.Generic (Data.Mergeless.Collection.ClientStore i a)
+ Data.Mergeless.Collection: instance GHC.Generics.Generic (Data.Mergeless.Collection.ServerStore i a)
+ Data.Mergeless.Collection: instance GHC.Generics.Generic (Data.Mergeless.Collection.ServerSyncProcessor i a m)
+ Data.Mergeless.Collection: instance GHC.Generics.Generic (Data.Mergeless.Collection.SyncRequest i a)
+ Data.Mergeless.Collection: instance GHC.Generics.Generic (Data.Mergeless.Collection.SyncResponse i a)
+ Data.Mergeless.Collection: instance GHC.Generics.Generic Data.Mergeless.Collection.ClientId
+ Data.Mergeless.Collection: instance GHC.Show.Show Data.Mergeless.Collection.ClientId
+ Data.Mergeless.Collection: makeSyncRequest :: (Ord i, Ord a) => ClientStore i a -> SyncRequest i a
+ Data.Mergeless.Collection: mergeSyncResponse :: forall i a. (Ord i, Ord a) => ClientStore i a -> SyncResponse i a -> ClientStore i a
+ Data.Mergeless.Collection: newtype ClientId
+ Data.Mergeless.Collection: newtype ServerStore i a
+ Data.Mergeless.Collection: processServerSync :: forall m i a. (Ord i, Ord a, Monad m) => m i -> ServerStore i a -> SyncRequest i a -> m (SyncResponse i a, ServerStore i a)
+ Data.Mergeless.Collection: processServerSyncCustom :: forall i a m. (Ord i, Ord a, Monad m) => ServerSyncProcessor i a m -> SyncRequest i a -> m (SyncResponse i a)
+ Data.Mergeless.Collection: storeSize :: ClientStore i a -> Int
+ Data.Mergeless.Item: ClientAdded :: !a -> ClientItem a
+ Data.Mergeless.Item: ClientDeleted :: ClientItem a
+ Data.Mergeless.Item: ClientEmpty :: ClientItem a
+ Data.Mergeless.Item: ClientSynced :: !a -> ClientItem a
+ Data.Mergeless.Item: ItemSyncRequestDeleted :: ItemSyncRequest a
+ Data.Mergeless.Item: ItemSyncRequestKnown :: ItemSyncRequest a
+ Data.Mergeless.Item: ItemSyncRequestNew :: a -> ItemSyncRequest a
+ Data.Mergeless.Item: ItemSyncRequestPoll :: ItemSyncRequest a
+ Data.Mergeless.Item: ItemSyncResponseClientAdded :: ItemSyncResponse a
+ Data.Mergeless.Item: ItemSyncResponseClientDeleted :: ItemSyncResponse a
+ Data.Mergeless.Item: ItemSyncResponseInSyncEmpty :: ItemSyncResponse a
+ Data.Mergeless.Item: ItemSyncResponseInSyncFull :: ItemSyncResponse a
+ Data.Mergeless.Item: ItemSyncResponseServerAdded :: !a -> ItemSyncResponse a
+ Data.Mergeless.Item: ItemSyncResponseServerDeleted :: ItemSyncResponse a
+ Data.Mergeless.Item: ServerItemEmpty :: ServerItem a
+ Data.Mergeless.Item: ServerItemFull :: !a -> ServerItem a
+ Data.Mergeless.Item: data ClientItem a
+ Data.Mergeless.Item: data ItemSyncRequest a
+ Data.Mergeless.Item: data ItemSyncResponse a
+ Data.Mergeless.Item: data ServerItem a
+ Data.Mergeless.Item: instance Control.DeepSeq.NFData a => Control.DeepSeq.NFData (Data.Mergeless.Item.ClientItem a)
+ Data.Mergeless.Item: instance Control.DeepSeq.NFData a => Control.DeepSeq.NFData (Data.Mergeless.Item.ItemSyncRequest a)
+ Data.Mergeless.Item: instance Control.DeepSeq.NFData a => Control.DeepSeq.NFData (Data.Mergeless.Item.ItemSyncResponse a)
+ Data.Mergeless.Item: instance Control.DeepSeq.NFData a => Control.DeepSeq.NFData (Data.Mergeless.Item.ServerItem a)
+ Data.Mergeless.Item: instance Data.Aeson.Types.FromJSON.FromJSON a => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeless.Item.ClientItem a)
+ Data.Mergeless.Item: instance Data.Aeson.Types.FromJSON.FromJSON a => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeless.Item.ItemSyncRequest a)
+ Data.Mergeless.Item: instance Data.Aeson.Types.FromJSON.FromJSON a => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeless.Item.ItemSyncResponse a)
+ Data.Mergeless.Item: instance Data.Aeson.Types.FromJSON.FromJSON a => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeless.Item.ServerItem a)
+ Data.Mergeless.Item: instance Data.Aeson.Types.ToJSON.ToJSON a => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeless.Item.ClientItem a)
+ Data.Mergeless.Item: instance Data.Aeson.Types.ToJSON.ToJSON a => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeless.Item.ItemSyncRequest a)
+ Data.Mergeless.Item: instance Data.Aeson.Types.ToJSON.ToJSON a => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeless.Item.ItemSyncResponse a)
+ Data.Mergeless.Item: instance Data.Aeson.Types.ToJSON.ToJSON a => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeless.Item.ServerItem a)
+ Data.Mergeless.Item: instance Data.Validity.Validity a => Data.Validity.Validity (Data.Mergeless.Item.ClientItem a)
+ Data.Mergeless.Item: instance Data.Validity.Validity a => Data.Validity.Validity (Data.Mergeless.Item.ItemSyncRequest a)
+ Data.Mergeless.Item: instance Data.Validity.Validity a => Data.Validity.Validity (Data.Mergeless.Item.ItemSyncResponse a)
+ Data.Mergeless.Item: instance Data.Validity.Validity a => Data.Validity.Validity (Data.Mergeless.Item.ServerItem a)
+ Data.Mergeless.Item: instance GHC.Classes.Eq a => GHC.Classes.Eq (Data.Mergeless.Item.ClientItem a)
+ Data.Mergeless.Item: instance GHC.Classes.Eq a => GHC.Classes.Eq (Data.Mergeless.Item.ItemSyncRequest a)
+ Data.Mergeless.Item: instance GHC.Classes.Eq a => GHC.Classes.Eq (Data.Mergeless.Item.ItemSyncResponse a)
+ Data.Mergeless.Item: instance GHC.Classes.Eq a => GHC.Classes.Eq (Data.Mergeless.Item.ServerItem a)
+ Data.Mergeless.Item: instance GHC.Classes.Ord a => GHC.Classes.Ord (Data.Mergeless.Item.ClientItem a)
+ Data.Mergeless.Item: instance GHC.Classes.Ord a => GHC.Classes.Ord (Data.Mergeless.Item.ItemSyncRequest a)
+ Data.Mergeless.Item: instance GHC.Classes.Ord a => GHC.Classes.Ord (Data.Mergeless.Item.ItemSyncResponse a)
+ Data.Mergeless.Item: instance GHC.Classes.Ord a => GHC.Classes.Ord (Data.Mergeless.Item.ServerItem a)
+ Data.Mergeless.Item: instance GHC.Generics.Generic (Data.Mergeless.Item.ClientItem a)
+ Data.Mergeless.Item: instance GHC.Generics.Generic (Data.Mergeless.Item.ItemSyncRequest a)
+ Data.Mergeless.Item: instance GHC.Generics.Generic (Data.Mergeless.Item.ItemSyncResponse a)
+ Data.Mergeless.Item: instance GHC.Generics.Generic (Data.Mergeless.Item.ServerItem a)
+ Data.Mergeless.Item: instance GHC.Show.Show a => GHC.Show.Show (Data.Mergeless.Item.ClientItem a)
+ Data.Mergeless.Item: instance GHC.Show.Show a => GHC.Show.Show (Data.Mergeless.Item.ItemSyncRequest a)
+ Data.Mergeless.Item: instance GHC.Show.Show a => GHC.Show.Show (Data.Mergeless.Item.ItemSyncResponse a)
+ Data.Mergeless.Item: instance GHC.Show.Show a => GHC.Show.Show (Data.Mergeless.Item.ServerItem a)
+ Data.Mergeless.Item: makeItemSyncRequest :: ClientItem a -> ItemSyncRequest a
+ Data.Mergeless.Item: mergeItemSyncResponse :: ClientItem a -> ItemSyncResponse a -> ClientItem a
+ Data.Mergeless.Item: processServerItemSync :: ServerItem a -> ItemSyncRequest a -> (ItemSyncResponse a, ServerItem a)
Files
- ChangeLog.md +4/−0
- mergeless.cabal +9/−7
- src/Data/Mergeless.hs +3/−498
- src/Data/Mergeless/Collection.hs +435/−0
- src/Data/Mergeless/Item.hs +148/−0
ChangeLog.md view
@@ -1,3 +1,7 @@ # Changelog for mergeless ## Unreleased changes++## [0.2.0.0] - 2020-02-12++* Got rid of the UTCTimes. The client should take care of that.
mergeless.cabal view
@@ -1,11 +1,13 @@--- This file has been generated from package.yaml by hpack version 0.28.2.+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.33.0. -- -- see: https://github.com/sol/hpack ----- hash: 749159cfb0e485429b445319f97126579e191fed759cadc0903f884e813159a7+-- hash: 7b7549a7b1a7d5ca5767941adb11f773af2e6727a37c8fdceb9367c0147b4d91 name: mergeless-version: 0.1.0.0+version: 0.2.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@@ -14,10 +16,9 @@ copyright: Copyright: (c) 2018 Tom Sydney Kerckhove license: MIT build-type: Simple-cabal-version: >= 1.10 extra-source-files:- ChangeLog.md README.md+ ChangeLog.md source-repository head type: git@@ -26,6 +27,8 @@ library exposed-modules: Data.Mergeless+ Data.Mergeless.Collection+ Data.Mergeless.Item other-modules: Paths_mergeless hs-source-dirs:@@ -34,9 +37,8 @@ aeson , base >=4.7 && <5 , containers+ , deepseq , mtl- , time , validity , validity-containers- , validity-time default-language: Haskell2010
src/Data/Mergeless.hs view
@@ -1,500 +1,5 @@-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE ScopedTypeVariables #-}---- | A way to synchronise items without merge conflicts.------ This concept has a few requirements:------ * Items must be immutable.--- * Items must allow for a centrally unique identifier.--- * Identifiers for items must be generatable in such a way that they are certainly unique.------ Should mutation be a requirement, then it can be build such that it entails deleting the old version and creating a new version that is the modification of the old version.--------- There are a few obvious candidates for identifiers:------ * incremental identifiers--- * universally unique identifiers (recommended).------------ The typical 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 central server should operate as follows:------ * The server accepts a 'SyncRequest'.--- * The server performs operations according to the functionality of 'processSync'.--- * The server respons with a 'SyncResponse'.--------- A client should operate as follows:------ * 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 'mergeSyncResponse'. module Data.Mergeless- ( Added(..)- , Synced(..)- , StoreItem(..)- , Store(..)- , emptyStore- , storeSize- , addItemToStore- , deleteUnsynced- , deleteSynced- , SyncRequest(..)- , SyncResponse(..)- -- * Client-side Synchronisation- , makeSyncRequest- , mergeSyncResponse- -- * Server-side Synchronisation- -- ** General synchronisation- , SyncProcessor(..)- , processSyncCustom- -- ** Synchronisation with a simple central store- , CentralStore(..)- , CentralItem(..)- , processSyncWith- , processSync- ) where--import Control.Applicative-import Control.Monad.IO.Class-import Control.Monad.State.Strict-import Data.Aeson-import Data.List-import Data.Map.Strict (Map)-import qualified Data.Map.Strict as M-import Data.Maybe-import qualified Data.Set as S-import Data.Set (Set)-import Data.Time-import Data.Validity-import Data.Validity.Containers ()-import Data.Validity.Time ()-import GHC.Generics (Generic)--{-# 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- { addedValue :: !a- , addedCreated :: !UTCTime- } deriving (Show, Eq, Ord, Generic)--instance Validity a => Validity (Added a)--instance FromJSON a => FromJSON (Added a) where- parseJSON =- withObject "Added" $ \o -> Added <$> o .: "value" <*> o .: "added"--instance ToJSON a => ToJSON (Added a) where- toJSON Added {..} = object ["value" .= addedValue, "added" .= addedCreated]---- | A local item of type @a@ with an identifier of type @a@ that has been synchronised-data Synced i a = Synced- { syncedUuid :: i- , syncedValue :: !a- , syncedCreated :: !UTCTime- , syncedSynced :: !UTCTime- } deriving (Show, Eq, Ord, Generic)--instance (Validity i, Validity a) => Validity (Synced i a)--instance (FromJSON i, FromJSON a) => FromJSON (Synced i a) where- parseJSON =- withObject "Synced" $ \o ->- Synced <$> o .: "id" <*> o .: "value" <*> o .: "created" <*>- o .: "synced"--instance (ToJSON i, ToJSON a) => ToJSON (Synced i a) where- toJSON Synced {..} =- object- [ "id" .= syncedUuid- , "value" .= syncedValue- , "created" .= syncedCreated- , "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))- , syncRequestSyncedItems :: !(Set i)- , syncRequestUndeletedItems :: !(Set i)- } deriving (Show, Eq, Ord, Generic)--instance (Validity i, Validity a, Ord i, Ord a) =>- Validity (SyncRequest i a) where- validate SyncRequest {..} =- mconcat- [ annotate syncRequestAddedItems "syncRequestAddedItems"- , annotate syncRequestSyncedItems "syncRequestSyncedItems"- , annotate syncRequestUndeletedItems "syncRequestUndeletedItems"- , declare "the sync request items have distinct ids" $- distinct $- S.toList syncRequestSyncedItems ++- S.toList syncRequestUndeletedItems- ]--instance (FromJSON i, FromJSON a, Ord i, Ord a) =>- FromJSON (SyncRequest i a) where- parseJSON =- withObject "SyncRequest" $ \o ->- SyncRequest <$> o .: "unsynced" <*> o .: "synced" <*>- o .: "undeleted"--instance (ToJSON i, ToJSON a) => ToJSON (SyncRequest i a) where- toJSON SyncRequest {..} =- object- [ "unsynced" .= syncRequestAddedItems- , "synced" .= syncRequestSyncedItems- , "undeleted" .= syncRequestUndeletedItems- ]---- | A synchronisation response for items with identifiers of type @i@ and values of type @a@-data SyncResponse i a = SyncResponse- { syncResponseAddedItems :: !(Set (Synced i a))- , syncResponseNewRemoteItems :: !(Set (Synced i a))- , syncResponseItemsToBeDeletedLocally :: !(Set i)- } deriving (Show, Eq, Ord, Generic)--instance (Validity i, Validity a, Ord i, Ord a) =>- Validity (SyncResponse i a) where- validate SyncResponse {..} =- mconcat- [ annotate syncResponseAddedItems "syncResponseAddedItems"- , annotate syncResponseNewRemoteItems "syncResponseNewRemoteItems"- , annotate- syncResponseItemsToBeDeletedLocally- "syncResponseItemsToBeDeletedLocally"- , declare "the sync response items have distinct uuids" $- distinct $- map- syncedUuid- (S.toList syncResponseAddedItems ++- S.toList syncResponseNewRemoteItems) ++- S.toList syncResponseItemsToBeDeletedLocally- ]--instance (FromJSON i, FromJSON a, Ord i, Ord a) =>- FromJSON (SyncResponse i a) where- parseJSON =- withObject "SyncResponse" $ \o ->- SyncResponse <$> o .: "added" <*> o .: "new" <*> o .: "deleted"--instance (ToJSON i, ToJSON a) => ToJSON (SyncResponse i a) where- toJSON SyncResponse {..} =- object- [ "added" .= syncResponseAddedItems- , "new" .= syncResponseNewRemoteItems- , "deleted" .= syncResponseItemsToBeDeletedLocally- ]---- | Produce a synchronisation request for a client-side store.------ This request can then be sent to a central store for synchronisation.-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- }---- | Merge a synchronisation response back into a client-side store.-mergeSyncResponse ::- (Ord i, Ord a) => Store i a -> SyncResponse i a -> Store i a-mergeSyncResponse s SyncResponse {..} =- let withNewOwnItems =- flip mapSetMaybe (storeItems s) $ \si ->- case si of- UnsyncedItem Added {..} ->- case find- (\Synced {..} ->- syncedCreated == addedCreated &&- syncedValue == addedValue)- syncResponseAddedItems of- Nothing -> Just si -- If it wasn't added (for whatever reason), just leave it as unsynced- Just ii -> Just $ SyncedItem ii -- If it was added, then it becomes synced- SyncedItem ii ->- case find- (== syncedUuid ii)- syncResponseItemsToBeDeletedLocally of- 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- }---- | 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- , centralSynced :: !UTCTime- , centralCreated :: !UTCTime- } deriving (Show, Eq, Ord, Generic)--instance Validity a => Validity (CentralItem a)--instance FromJSON a => FromJSON (CentralItem a)--instance ToJSON a => ToJSON (CentralItem a)---- | A central store of items with identifiers of type @i@ and values of type @a@-newtype CentralStore i a = CentralStore- { centralStoreItems :: Map i (CentralItem a)- } deriving (Show, Eq, Ord, Generic, FromJSON, ToJSON)--instance (Validity i, Validity a, Ord i, Ord a) =>- Validity (CentralStore i a)---- | Process a server-side synchronisation request using @getCurrentTime@------ see 'processSyncCustom'-processSync ::- (Ord i, Ord a, MonadIO m)- => m i- -> CentralStore i a- -> SyncRequest i a- -> m (SyncResponse i a, CentralStore i a)-processSync genId cs sr = do- now <- liftIO getCurrentTime- processSyncWith genId now cs sr---- | Process a server-side synchronisation request using a time of syncing, and an identifier generation function.------ see 'processSyncCustom'-processSyncWith ::- forall i a m. (Ord i, Ord a, Monad m)- => m i- -> UTCTime- -> CentralStore i a- -> SyncRequest i a- -> m (SyncResponse i a, CentralStore i a)-processSyncWith genUuid now cs sr =- flip runStateT cs $- processSyncCustom- (lift genUuid)- now- SyncProcessor- { syncProcessorDeleteMany = deleteMany- , syncProcessorQuerySynced = querySynced- , syncProcessorQueryNewRemote = queryNewRemote- , syncProcessorInsertMany = insertMany- }- sr- where- deleteMany :: Set i -> StateT (CentralStore i a) m ()- deleteMany s = modC (`M.withoutKeys` s)- 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 m) $ \(i, CentralItem {..}) ->- Synced- { 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- insertMany :: Set (Synced i a) -> StateT (CentralStore i a) m ()- insertMany s =- forM_ (S.toList s) $ \Synced {..} ->- ins- syncedUuid- CentralItem- { centralValue = syncedValue- , centralCreated = syncedCreated- , centralSynced = syncedSynced- }- ins :: i -> CentralItem a -> StateT (CentralStore i a) m ()- ins i val = modC $ M.insert i val- modC ::- (Map i (CentralItem a) -> Map i (CentralItem a))- -> StateT (CentralStore i a) m ()- modC func = modify (\(CentralStore m) -> CentralStore $ func m)--mapSetMaybe :: Ord b => (a -> Maybe b) -> Set a -> Set b-mapSetMaybe func = S.map fromJust . S.filter isJust . S.map func+ ( module Data.Mergeless.Collection+ ) where -distinct :: Ord a => [a] -> Bool-distinct ls = sort ls == S.toAscList (S.fromList ls)+import Data.Mergeless.Collection
+ src/Data/Mergeless/Collection.hs view
@@ -0,0 +1,435 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}++-- | A way to synchronise items without merge conflicts.+--+-- This concept has a few requirements:+--+-- * Items must be immutable.+-- * Items must allow for a centrally unique identifier.+-- * Identifiers for items must be generatable in such a way that they are certainly unique.+--+-- Should mutation be a requirement, then it can be build such that it entails deleting the old version and creating a new version that is the modification of the old version.+--+--+-- There are a few obvious candidates for identifiers:+--+-- * incremental identifiers+-- * universally unique identifiers (recommended).+--+--+--+-- The typical 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 central server should operate as follows:+--+-- * The server accepts a 'SyncRequest'.+-- * The server performs operations according to the functionality of 'processServerSync'.+-- * The server respons with a 'SyncResponse'.+--+--+-- A client should operate as follows:+--+-- * 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 'mergeSyncResponse'.+module Data.Mergeless.Collection+ ( ClientId(..)+ , ClientStore(..)+ , emptyClientStore+ , storeSize+ , addItemToClientStore+ , deleteUnsyncedFromClientStore+ , deleteSyncedFromClientStore+ , SyncRequest(..)+ , SyncResponse(..)+ , emptySyncResponse+ -- * Client-side Synchronisation+ , makeSyncRequest+ , mergeSyncResponse+ , addRemotelyAddedItems+ , addAddedItems+ , deleteItemsToBeDeletedLocally+ , deleteLocalUndeletedItems+ -- * Server-side Synchronisation+ -- ** General synchronisation+ , ServerSyncProcessor(..)+ , processServerSyncCustom+ -- ** Synchronisation with a simple central store+ , ServerStore(..)+ , emptyServerStore+ , processServerSync+ ) where++import GHC.Generics (Generic)++import Data.Validity+import Data.Validity.Containers ()++import Data.Aeson+import Data.List+import Data.Map.Strict (Map)+import qualified Data.Map.Strict as M+import qualified Data.Set as S+import Data.Set (Set)+import Data.Word++import Control.DeepSeq+import Control.Monad.State.Strict++{-# ANN module ("HLint: ignore Use lambda-case" :: String) #-}++-- | 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++instance NFData ClientId++-- | A client-side store of items with Id's of type @i@ and values of type @a@+data ClientStore i a =+ ClientStore+ { clientStoreAdded :: !(Map ClientId a)+ , clientStoreSynced :: !(Map i a)+ , clientStoreDeleted :: !(Set i)+ }+ deriving (Show, Eq, Ord, Generic)++instance (NFData i, NFData a) => NFData (ClientStore i a)++instance (Validity i, Validity a, Show i, Show a, Ord i, Ord a) => Validity (ClientStore i a) where+ validate cs@ClientStore {..} =+ mconcat+ [ genericValidate cs+ , declare "the store items have distinct ids" $+ distinct $ M.keys clientStoreSynced ++ S.toList clientStoreDeleted+ ]++instance (Ord i, FromJSON i, FromJSONKey i, FromJSON a) => FromJSON (ClientStore i a) where+ parseJSON =+ withObject "ClientStore" $ \o ->+ ClientStore <$> o .:? "added" .!= M.empty <*> o .:? "synced" .!= M.empty <*>+ o .:? "deleted" .!= S.empty++instance (Ord i, ToJSON i, ToJSONKey i, ToJSON a) => ToJSON (ClientStore i a) where+ toJSON ClientStore {..} =+ object+ ["added" .= clientStoreAdded, "synced" .= clientStoreSynced, "deleted" .= clientStoreDeleted]++-- | The store with no items.+emptyClientStore :: ClientStore i a+emptyClientStore =+ ClientStore+ {clientStoreAdded = M.empty, clientStoreSynced = M.empty, clientStoreDeleted = S.empty}++-- | The number of items in a store+--+-- This does not count the deleted items, so that those really look deleted.+storeSize :: ClientStore i a -> Int+storeSize ClientStore {..} = M.size clientStoreAdded + M.size clientStoreSynced++clientStoreIds :: Ord i => ClientStore i a -> Set i+clientStoreIds ClientStore {..} = M.keysSet clientStoreSynced `S.union` clientStoreDeleted++-- | 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 :: (Ord i, Ord a) => a -> ClientStore i a -> ClientStore i a+addItemToClientStore a cs =+ let oldAddedItems = clientStoreAdded cs+ newAddedItems =+ let newKey = findFreeSpot oldAddedItems+ in M.insert newKey a oldAddedItems+ in cs {clientStoreAdded = newAddedItems}++-- | Find a free client id to use+--+-- You shouldn't need this function, 'addItemToClientStore' takes care of this.+findFreeSpot :: Map ClientId a -> ClientId+findFreeSpot m =+ if M.null m+ then ClientId 0+ else let (i, _) = M.findMax m+ in go (next i)+ where+ go i =+ if M.member i m+ then go (next i)+ else i+ next (ClientId w)+ | w == maxBound = ClientId 0+ | otherwise = ClientId $ succ w++deleteUnsyncedFromClientStore :: (Ord i, Ord a) => ClientId -> ClientStore i a -> ClientStore i a+deleteUnsyncedFromClientStore cid cs = cs {clientStoreAdded = M.delete cid $ clientStoreAdded cs}++deleteSyncedFromClientStore :: (Ord i, Ord a) => i -> ClientStore i a -> ClientStore i a+deleteSyncedFromClientStore i cs =+ let syncedBefore = clientStoreSynced cs+ in case M.lookup i syncedBefore of+ Nothing -> cs+ Just _ ->+ cs+ { clientStoreSynced = M.delete i syncedBefore+ , clientStoreDeleted = S.insert i $ clientStoreDeleted cs+ }++-- | A synchronisation request for items with identifiers of type @i@ and values of type @a@+data SyncRequest i a =+ SyncRequest+ { syncRequestAdded :: !(Map ClientId a)+ , syncRequestSynced :: !(Set i)+ , syncRequestDeleted :: !(Set i)+ }+ deriving (Show, Eq, Ord, Generic)++instance (NFData i, NFData a) => NFData (SyncRequest i a)++instance (Validity i, Validity a, Ord i, Ord a) => Validity (SyncRequest i a) where+ validate sr@SyncRequest {..} =+ mconcat+ [ genericValidate sr+ , declare "the sync request items have distinct ids" $+ distinct $ S.toList syncRequestSynced ++ S.toList syncRequestDeleted+ ]++instance (FromJSON i, FromJSON a, Ord i, Ord a) => FromJSON (SyncRequest i a) where+ parseJSON =+ withObject "SyncRequest" $ \o ->+ SyncRequest <$> o .: "added" <*> o .: "synced" <*> o .: "undeleted"++instance (ToJSON i, ToJSON a) => ToJSON (SyncRequest i a) where+ toJSON SyncRequest {..} =+ object+ [ "added" .= syncRequestAdded+ , "synced" .= syncRequestSynced+ , "undeleted" .= syncRequestDeleted+ ]++-- | Produce a synchronisation request for a client-side store.+--+-- This request can then be sent to a central store for synchronisation.+makeSyncRequest :: (Ord i, Ord a) => ClientStore i a -> SyncRequest i a+makeSyncRequest ClientStore {..} =+ SyncRequest+ { syncRequestAdded = clientStoreAdded+ , syncRequestSynced = M.keysSet clientStoreSynced+ , syncRequestDeleted = clientStoreDeleted+ }++-- | A synchronisation response for items with identifiers of type @i@ and values of type @a@+data SyncResponse i a =+ SyncResponse+ { syncResponseClientAdded :: !(Map ClientId i)+ , syncResponseClientDeleted :: !(Set i)+ , syncResponseServerAdded :: !(Map i a)+ , syncResponseServerDeleted :: !(Set i)+ }+ deriving (Show, Eq, Ord, Generic)++instance (NFData i, NFData a) => NFData (SyncResponse i a)++instance (Validity i, Validity a, Show i, Show a, Ord i, Ord a) => Validity (SyncResponse i a) where+ validate sr@SyncResponse {..} =+ mconcat+ [ genericValidate sr+ , declare "the sync response items have distinct uuids" $+ distinct $+ concat+ [ M.elems syncResponseClientAdded+ , S.toList syncResponseClientDeleted+ , M.keys syncResponseServerAdded+ , S.toList syncResponseServerDeleted+ ]+ ]++instance (Ord i, FromJSON i, FromJSONKey i, Ord a, FromJSON a) => FromJSON (SyncResponse i a) where+ parseJSON =+ withObject "SyncResponse" $ \o ->+ SyncResponse <$> o .: "client-added" <*> o .: "client-deleted" <*> o .: "server-added" <*>+ o .: "server-deleted"++instance (ToJSON i, ToJSONKey i, ToJSON a) => ToJSON (SyncResponse i a) where+ toJSON SyncResponse {..} =+ object+ [ "client-added" .= syncResponseClientAdded+ , "client-deleted" .= syncResponseClientDeleted+ , "server-added" .= syncResponseServerAdded+ , "server-deleted" .= syncResponseServerDeleted+ ]++emptySyncResponse :: SyncResponse i ia+emptySyncResponse =+ SyncResponse+ { syncResponseClientAdded = M.empty+ , syncResponseClientDeleted = S.empty+ , syncResponseServerAdded = M.empty+ , syncResponseServerDeleted = S.empty+ }++-- | Merge a synchronisation response back into a client-side store.+mergeSyncResponse ::+ forall i a. (Ord i, Ord a)+ => ClientStore i a+ -> SyncResponse i a+ -> ClientStore i a+mergeSyncResponse s SyncResponse {..} =+ addRemotelyAddedItems syncResponseServerAdded .+ addAddedItems syncResponseClientAdded .+ deleteItemsToBeDeletedLocally syncResponseServerDeleted .+ deleteLocalUndeletedItems syncResponseClientDeleted $+ s++addRemotelyAddedItems :: (Ord i, Ord a) => Map i a -> ClientStore i a -> ClientStore i a+addRemotelyAddedItems m cs =+ cs {clientStoreSynced = M.union (clientStoreSynced cs) (m `diffSet` clientStoreIds cs)}++addAddedItems ::+ forall i a. (Ord i, Ord a)+ => Map ClientId i+ -> ClientStore i a+ -> ClientStore i a+addAddedItems addedItems cs =+ let oldAdded = clientStoreAdded cs+ oldSynced = clientStoreSynced cs+ go :: (Map ClientId a, Map i a) -> ClientId -> i -> (Map ClientId a, Map i a)+ go (added, synced) cid i =+ case M.lookup cid added of+ Nothing -> (added, synced)+ Just a -> (M.delete cid added, M.insert i a synced)+ (newAdded, newSynced) = M.foldlWithKey go (oldAdded, oldSynced) addedItems+ in cs {clientStoreAdded = newAdded, clientStoreSynced = newSynced}++deleteItemsToBeDeletedLocally :: (Ord i, Ord a) => Set i -> ClientStore i a -> ClientStore i a+deleteItemsToBeDeletedLocally toBeDeletedLocally cs =+ cs {clientStoreSynced = clientStoreSynced cs `diffSet` toBeDeletedLocally}++deleteLocalUndeletedItems :: (Ord i, Ord a) => Set i -> ClientStore i a -> ClientStore i a+deleteLocalUndeletedItems cd cs = cs {clientStoreDeleted = clientStoreDeleted cs `S.difference` cd}++-- | A record of the basic operations that are necessary to build a synchronisation processor.+data ServerSyncProcessor i a m =+ ServerSyncProcessor+ { serverSyncProcessorDeleteMany :: Set i -> m (Set i)+ -- ^ Delete the items with an identifier in the given set, return the set that was indeed deleted or did not exist.+ -- In particular, return the identifiers of the items that the client should forget about.+ , serverSyncProcessorQueryNoLongerSynced :: Set i -> m (Set i) -- ^ Query the identifiers of the items that are in the given set but not in the store.+ , serverSyncProcessorQueryNewRemote :: Set i -> m (Map i a) -- ^ Query the items that are in store, but not in the given set.+ , serverSyncProcessorInsertMany :: Map ClientId a -> m (Map ClientId i) -- ^ Insert a set of items into the store.+ }+ 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.+processServerSyncCustom ::+ forall i a m. (Ord i, Ord a, Monad m)+ => ServerSyncProcessor i a m+ -> SyncRequest i a+ -> m (SyncResponse i a)+processServerSyncCustom ServerSyncProcessor {..} SyncRequest {..} = do+ deletedFromClient <- 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+ { syncResponseClientAdded = newLocalItems+ , syncResponseClientDeleted = deletedFromClient+ , syncResponseServerAdded = newRemoteItems+ , syncResponseServerDeleted = deletedRemotely+ }+ where+ deleteUndeleted :: m (Set i)+ deleteUndeleted = serverSyncProcessorDeleteMany syncRequestDeleted+ syncItemsToBeDeletedLocally :: m (Set i)+ syncItemsToBeDeletedLocally = serverSyncProcessorQueryNoLongerSynced syncRequestSynced+ syncNewRemoteItems :: m (Map i a)+ syncNewRemoteItems = serverSyncProcessorQueryNewRemote syncRequestSynced+ syncAddedItems :: m (Map ClientId i)+ syncAddedItems = serverSyncProcessorInsertMany syncRequestAdded++-- | A central store of items with identifiers of type @i@ and values of type @a@+newtype ServerStore i a =+ ServerStore+ { serverStoreItems :: Map i a+ }+ deriving (Show, Eq, Ord, Generic, FromJSON, ToJSON)++instance (NFData i, NFData a) => NFData (ServerStore i a)++instance (Validity i, Validity a, Show i, Show a, Ord i, Ord a) => Validity (ServerStore i a)++-- | An empty central store to start with+emptyServerStore :: ServerStore i a+emptyServerStore = ServerStore {serverStoreItems = M.empty}++-- | Process a server-side synchronisation request using @getCurrentTime@+--+-- see 'processSyncCustom'+processServerSync ::+ forall m i a. (Ord i, Ord a, Monad m)+ => m i+ -> ServerStore i a+ -> SyncRequest i a+ -> m (SyncResponse i a, ServerStore i a)+processServerSync genUuid cs sr =+ flip runStateT cs $+ processServerSyncCustom+ ServerSyncProcessor+ { serverSyncProcessorDeleteMany = deleteMany+ , serverSyncProcessorQueryNoLongerSynced = queryNoLongerSynced+ , serverSyncProcessorQueryNewRemote = queryNewRemote+ , serverSyncProcessorInsertMany = insertMany+ }+ sr+ where+ deleteMany :: Set i -> StateT (ServerStore i a) m (Set i)+ deleteMany s = do+ modC (`diffSet` s)+ pure s+ queryNoLongerSynced :: Set i -> StateT (ServerStore i a) m (Set i)+ queryNoLongerSynced s = query ((s `S.difference`) . M.keysSet)+ queryNewRemote :: Set i -> StateT (ServerStore i a) m (Map i a)+ queryNewRemote s = query (`diffSet` s)+ query :: (Map i a -> b) -> StateT (ServerStore i a) m b+ query func = gets $ func . serverStoreItems+ insertMany :: Map ClientId a -> StateT (ServerStore i a) m (Map ClientId i)+ insertMany =+ traverse $ \a -> do+ u <- lift genUuid+ ins u a+ pure u+ ins :: i -> a -> StateT (ServerStore i a) m ()+ ins i val = modC $ M.insert i val+ modC :: (Map i a -> Map i a) -> StateT (ServerStore i a) m ()+ modC func = modify (\(ServerStore m) -> ServerStore $ func m)++diffSet :: Ord i => Map i a -> Set i -> Map i a+diffSet m s = m `M.difference` toMap s++toMap :: Set i -> Map i ()+toMap = M.fromSet (const ())++distinct :: Ord a => [a] -> Bool+distinct ls = sort ls == S.toAscList (S.fromList ls)
+ src/Data/Mergeless/Item.hs view
@@ -0,0 +1,148 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Data.Mergeless.Item where++import Control.DeepSeq+import Data.Aeson+import Data.Validity+import Data.Validity.Containers ()+import GHC.Generics (Generic)++{-# ANN module ("HLint: ignore Use lambda-case" :: String) #-}++data ClientItem a+ = ClientEmpty+ | ClientAdded !a+ | ClientSynced !a+ | ClientDeleted+ deriving (Show, Eq, Ord, Generic)++instance Validity a => Validity (ClientItem a)++instance NFData a => NFData (ClientItem a)++instance FromJSON a => FromJSON (ClientItem a)++instance ToJSON a => ToJSON (ClientItem a)++-- | A synchronisation request for items with identifiers of type @i@ and values of type @a@+data ItemSyncRequest a+ = ItemSyncRequestPoll+ | ItemSyncRequestNew a+ | ItemSyncRequestKnown+ | ItemSyncRequestDeleted+ deriving (Show, Eq, Ord, Generic)++instance Validity a => Validity (ItemSyncRequest a)++instance NFData a => NFData (ItemSyncRequest a)++instance FromJSON a => FromJSON (ItemSyncRequest a)++instance ToJSON a => ToJSON (ItemSyncRequest a)++makeItemSyncRequest :: ClientItem a -> ItemSyncRequest a+makeItemSyncRequest ci =+ case ci of+ ClientEmpty -> ItemSyncRequestPoll+ ClientAdded a -> ItemSyncRequestNew a+ ClientSynced _ -> ItemSyncRequestKnown+ ClientDeleted -> ItemSyncRequestDeleted++-- | A synchronisation response for items with identifiers of type @i@ and values of type @a@+data ItemSyncResponse a+ = ItemSyncResponseInSyncEmpty+ | ItemSyncResponseInSyncFull+ | ItemSyncResponseClientAdded+ | ItemSyncResponseClientDeleted+ | ItemSyncResponseServerAdded !a+ | ItemSyncResponseServerDeleted+ deriving (Show, Eq, Ord, Generic)++instance Validity a => Validity (ItemSyncResponse a)++instance NFData a => NFData (ItemSyncResponse a)++instance FromJSON a => FromJSON (ItemSyncResponse a)++instance ToJSON a => ToJSON (ItemSyncResponse a)++-- | Merge a synchronisation response back into a client-side store.+mergeItemSyncResponse :: ClientItem a -> ItemSyncResponse a -> ClientItem a+mergeItemSyncResponse ci sr =+ let mismatch = ci+ in case ci of+ ClientEmpty ->+ case sr of+ ItemSyncResponseInSyncEmpty -> ClientEmpty+ ItemSyncResponseServerAdded s -> ClientSynced s+ _ -> mismatch+ ClientAdded a ->+ case sr of+ ItemSyncResponseClientAdded -> ClientSynced a+ ItemSyncResponseServerAdded s -> ClientSynced s+ -- For completeness sake.+ -- This can only happen if two clients make the item at the same time.+ -- In practice, with named items in a collection, this will never happen.+ _ -> mismatch+ ClientSynced _ ->+ case sr of+ ItemSyncResponseInSyncFull -> ci -- No change+ ItemSyncResponseServerDeleted -> ClientEmpty+ _ -> mismatch+ ClientDeleted ->+ case sr of+ ItemSyncResponseClientDeleted -> ClientEmpty+ _ -> mismatch++-- | An item in a central store with a value of type @a@+data ServerItem a+ = ServerItemEmpty+ | ServerItemFull !a+ deriving (Show, Eq, Ord, Generic)++instance Validity a => Validity (ServerItem a)++instance NFData a => NFData (ServerItem a)++instance FromJSON a => FromJSON (ServerItem a)++instance ToJSON a => ToJSON (ServerItem a)++processServerItemSync :: ServerItem a -> ItemSyncRequest a -> (ItemSyncResponse a, ServerItem a)+processServerItemSync si sr =+ case si of+ ServerItemEmpty ->+ case sr of+ ItemSyncRequestPoll+ -- Both the client and the server think the item is empty, fine.+ -> (ItemSyncResponseInSyncEmpty, si)+ ItemSyncRequestNew a+ -- The client has a new item and the server has space for it, add it.+ -> (ItemSyncResponseClientAdded, ServerItemFull a)+ ItemSyncRequestKnown+ -- The client has an item that the server doesn't, so the server must have+ -- deleted it when another client asked to do that.+ -- Leave it deleted.+ -> (ItemSyncResponseServerDeleted, si)+ ItemSyncRequestDeleted+ -- The server has deleted an item but the current client hasn't been made aware of that+ -- AND this server also deleted that item in the meantime.+ -- Just leave it deleted.+ -> (ItemSyncResponseClientDeleted, si)+ ServerItemFull s ->+ case sr of+ ItemSyncRequestPoll+ -- The server has an item that the client doesn't, send it to the client.+ -> (ItemSyncResponseServerAdded s, si)+ ItemSyncRequestNew _+ -- The client wants to add an item that the server already has.+ -- That means that another client has added that same item in the meantime.+ -- This wouldn't happen if the items were named.+ -- In this case, for completeness sake,+ -> (ItemSyncResponseServerAdded s, si)+ ItemSyncRequestKnown -> (ItemSyncResponseInSyncFull, si)+ ItemSyncRequestDeleted -> (ItemSyncResponseClientDeleted, ServerItemEmpty)