mergeful 0.1.0.0 → 0.2.0.0
raw patch · 7 files changed
+1450/−1123 lines, 7 filesdep +deepseqPVP ok
version bump matches the API change (PVP)
Dependencies added: deepseq
API changes (from Hackage documentation)
- Data.Mergeful.Collection: addToSyncResponse :: Ord i => SyncResponse i a -> Identifier i -> ItemSyncResponse a -> SyncResponse 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.Validity.Validity i => Data.Validity.Validity (Data.Mergeful.Collection.Identifier i)
- Data.Mergeful.Collection: instance GHC.Classes.Eq i => GHC.Classes.Eq (Data.Mergeful.Collection.Identifier i)
- Data.Mergeful.Collection: instance GHC.Classes.Ord i => GHC.Classes.Ord (Data.Mergeful.Collection.Identifier i)
- 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.Show.Show i => GHC.Show.Show (Data.Mergeful.Collection.Identifier i)
- Data.Mergeful.Collection: mergeSyncResponseIgnoreProblems :: Ord i => ClientStore i a -> SyncResponse i a -> ClientStore i a
- Data.Mergeful.Item: mergeIgnoringProblems :: ClientItem a -> ItemMergeResult a -> ClientItem a
- Data.Mergeful.Item: mergeItemSyncResponseIgnoreProblems :: ClientItem a -> ItemSyncResponse a -> ClientItem a
- Data.Mergeful.Timed: data ServerTime
+ Data.Mergeful.Collection: ClientAddition :: i -> ServerTime -> ClientAddition i
+ Data.Mergeful.Collection: ClientSyncProcessor :: !Set si -> m (Map si (Timed a)) -> !Map ci (ClientAddition si) -> m () -> !Map si ServerTime -> m () -> !Set si -> m () -> !Map si (Timed a) -> m () -> !Map si (Timed a) -> m () -> !Map si (Timed a) -> m () -> !Set si -> m () -> ClientSyncProcessor ci si a
+ Data.Mergeful.Collection: Delete :: ServerDeletedConflictResolution
+ Data.Mergeful.Collection: KeepLocal :: ChangeConflictResolution a
+ Data.Mergeful.Collection: KeepLocalChange :: ServerDeletedConflictResolution
+ Data.Mergeful.Collection: Merged :: a -> ChangeConflictResolution a
+ Data.Mergeful.Collection: ServerSyncProcessor :: !m (Map si (Timed a)) -> !a -> m si -> !si -> ServerTime -> a -> m () -> !si -> m () -> ServerSyncProcessor ci si a m
+ Data.Mergeful.Collection: ServerTime :: Word64 -> ServerTime
+ Data.Mergeful.Collection: StayDeleted :: ClientDeletedConflictResolution
+ Data.Mergeful.Collection: TakeRemote :: ChangeConflictResolution a
+ Data.Mergeful.Collection: TakeRemoteChange :: ClientDeletedConflictResolution
+ Data.Mergeful.Collection: Timed :: !a -> !ServerTime -> Timed a
+ Data.Mergeful.Collection: [clientAdditionId] :: ClientAddition i -> i
+ Data.Mergeful.Collection: [clientAdditionServerTime] :: ClientAddition i -> ServerTime
+ Data.Mergeful.Collection: [clientSyncProcessorQuerySyncedButChangedValues] :: ClientSyncProcessor ci si a -> !Set si -> m (Map si (Timed a))
+ Data.Mergeful.Collection: [clientSyncProcessorSyncClientAdded] :: ClientSyncProcessor ci si a -> !Map ci (ClientAddition si) -> m ()
+ Data.Mergeful.Collection: [clientSyncProcessorSyncClientChanged] :: ClientSyncProcessor ci si a -> !Map si ServerTime -> m ()
+ Data.Mergeful.Collection: [clientSyncProcessorSyncClientDeleted] :: ClientSyncProcessor ci si a -> !Set si -> m ()
+ Data.Mergeful.Collection: [clientSyncProcessorSyncMergedConflict] :: ClientSyncProcessor ci si a -> !Map si (Timed a) -> m ()
+ Data.Mergeful.Collection: [clientSyncProcessorSyncServerAdded] :: ClientSyncProcessor ci si a -> !Map si (Timed a) -> m ()
+ Data.Mergeful.Collection: [clientSyncProcessorSyncServerChanged] :: ClientSyncProcessor ci si a -> !Map si (Timed a) -> m ()
+ Data.Mergeful.Collection: [clientSyncProcessorSyncServerDeleted] :: ClientSyncProcessor ci si a -> !Set si -> m ()
+ Data.Mergeful.Collection: [serverSyncProcessorAddItem] :: ServerSyncProcessor ci si a m -> !a -> m si
+ Data.Mergeful.Collection: [serverSyncProcessorChangeItem] :: ServerSyncProcessor ci si a m -> !si -> ServerTime -> a -> m ()
+ Data.Mergeful.Collection: [serverSyncProcessorDeleteItem] :: ServerSyncProcessor ci si a m -> !si -> m ()
+ Data.Mergeful.Collection: [serverSyncProcessorRead] :: ServerSyncProcessor ci si a m -> !m (Map si (Timed a))
+ Data.Mergeful.Collection: [timedTime] :: Timed a -> !ServerTime
+ Data.Mergeful.Collection: [timedValue] :: Timed a -> !a
+ Data.Mergeful.Collection: [unServerTime] :: ServerTime -> Word64
+ Data.Mergeful.Collection: data ChangeConflictResolution a
+ Data.Mergeful.Collection: data ClientAddition i
+ Data.Mergeful.Collection: data ClientDeletedConflictResolution
+ Data.Mergeful.Collection: data ClientSyncProcessor ci si a (m :: * -> *)
+ Data.Mergeful.Collection: data ServerDeletedConflictResolution
+ Data.Mergeful.Collection: data ServerSyncProcessor ci si a m
+ Data.Mergeful.Collection: data Timed a
+ Data.Mergeful.Collection: findFreeSpot :: (Ord ci, Enum ci, Bounded ci) => Map ci a -> ci
+ Data.Mergeful.Collection: incrementServerTime :: ServerTime -> ServerTime
+ Data.Mergeful.Collection: initialServerTime :: ServerTime
+ Data.Mergeful.Collection: instance (Control.DeepSeq.NFData ci, Control.DeepSeq.NFData si) => Control.DeepSeq.NFData (Data.Mergeful.Collection.Identifier ci si)
+ Data.Mergeful.Collection: instance (Control.DeepSeq.NFData ci, Control.DeepSeq.NFData si, Control.DeepSeq.NFData a) => Control.DeepSeq.NFData (Data.Mergeful.Collection.ClientStore ci si a)
+ Data.Mergeful.Collection: instance (Control.DeepSeq.NFData ci, Control.DeepSeq.NFData si, Control.DeepSeq.NFData a) => Control.DeepSeq.NFData (Data.Mergeful.Collection.SyncRequest ci si a)
+ Data.Mergeful.Collection: instance (Control.DeepSeq.NFData ci, Control.DeepSeq.NFData si, Control.DeepSeq.NFData a) => Control.DeepSeq.NFData (Data.Mergeful.Collection.SyncResponse ci si a)
+ Data.Mergeful.Collection: instance (Control.DeepSeq.NFData si, Control.DeepSeq.NFData a) => Control.DeepSeq.NFData (Data.Mergeful.Collection.ServerStore si a)
+ Data.Mergeful.Collection: instance (Data.Aeson.Types.FromJSON.FromJSONKey si, GHC.Classes.Ord si, Data.Aeson.Types.FromJSON.FromJSON a) => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeful.Collection.ServerStore si a)
+ Data.Mergeful.Collection: instance (Data.Aeson.Types.ToJSON.ToJSON a, Data.Aeson.Types.ToJSON.ToJSONKey si) => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeful.Collection.ServerStore si a)
+ Data.Mergeful.Collection: instance (Data.Aeson.Types.ToJSON.ToJSON ci, Data.Aeson.Types.ToJSON.ToJSON si, Data.Aeson.Types.ToJSON.ToJSONKey ci, Data.Aeson.Types.ToJSON.ToJSONKey si, Data.Aeson.Types.ToJSON.ToJSON a) => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeful.Collection.SyncResponse ci si a)
+ Data.Mergeful.Collection: instance (Data.Aeson.Types.ToJSON.ToJSONKey ci, Data.Aeson.Types.ToJSON.ToJSONKey si, Data.Aeson.Types.ToJSON.ToJSON a) => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeful.Collection.ClientStore ci si a)
+ Data.Mergeful.Collection: instance (Data.Aeson.Types.ToJSON.ToJSONKey ci, Data.Aeson.Types.ToJSON.ToJSONKey si, Data.Aeson.Types.ToJSON.ToJSON a) => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeful.Collection.SyncRequest ci si a)
+ Data.Mergeful.Collection: instance (Data.Validity.Validity ci, Data.Validity.Validity si) => Data.Validity.Validity (Data.Mergeful.Collection.Identifier ci si)
+ Data.Mergeful.Collection: instance (Data.Validity.Validity ci, Data.Validity.Validity si, GHC.Show.Show ci, GHC.Show.Show si, GHC.Classes.Ord ci, GHC.Classes.Ord si, Data.Validity.Validity a) => Data.Validity.Validity (Data.Mergeful.Collection.ClientStore ci si a)
+ Data.Mergeful.Collection: instance (Data.Validity.Validity ci, Data.Validity.Validity si, GHC.Show.Show ci, GHC.Show.Show si, GHC.Classes.Ord ci, GHC.Classes.Ord si, Data.Validity.Validity a) => Data.Validity.Validity (Data.Mergeful.Collection.SyncRequest ci si a)
+ Data.Mergeful.Collection: instance (Data.Validity.Validity ci, Data.Validity.Validity si, GHC.Show.Show ci, GHC.Show.Show si, GHC.Classes.Ord ci, GHC.Classes.Ord si, Data.Validity.Validity a) => Data.Validity.Validity (Data.Mergeful.Collection.SyncResponse ci si a)
+ Data.Mergeful.Collection: instance (Data.Validity.Validity si, GHC.Show.Show si, GHC.Classes.Ord si, Data.Validity.Validity a) => Data.Validity.Validity (Data.Mergeful.Collection.ServerStore si a)
+ Data.Mergeful.Collection: instance (GHC.Classes.Eq ci, GHC.Classes.Eq a, GHC.Classes.Eq si) => GHC.Classes.Eq (Data.Mergeful.Collection.ClientStore ci si a)
+ Data.Mergeful.Collection: instance (GHC.Classes.Eq ci, GHC.Classes.Eq a, GHC.Classes.Eq si) => GHC.Classes.Eq (Data.Mergeful.Collection.SyncRequest ci si a)
+ Data.Mergeful.Collection: instance (GHC.Classes.Eq ci, GHC.Classes.Eq si, GHC.Classes.Eq a) => GHC.Classes.Eq (Data.Mergeful.Collection.SyncResponse ci si a)
+ Data.Mergeful.Collection: instance (GHC.Classes.Eq si, GHC.Classes.Eq a) => GHC.Classes.Eq (Data.Mergeful.Collection.ServerStore si a)
+ Data.Mergeful.Collection: instance (GHC.Classes.Eq si, GHC.Classes.Eq ci) => GHC.Classes.Eq (Data.Mergeful.Collection.Identifier ci si)
+ Data.Mergeful.Collection: instance (GHC.Classes.Ord ci, GHC.Classes.Ord si, Data.Aeson.Types.FromJSON.FromJSON ci, Data.Aeson.Types.FromJSON.FromJSON si, Data.Aeson.Types.FromJSON.FromJSONKey ci, Data.Aeson.Types.FromJSON.FromJSONKey si, Data.Aeson.Types.FromJSON.FromJSON a) => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeful.Collection.SyncResponse ci si a)
+ Data.Mergeful.Collection: instance (GHC.Classes.Ord ci, GHC.Classes.Ord si, Data.Aeson.Types.FromJSON.FromJSONKey ci, Data.Aeson.Types.FromJSON.FromJSONKey si, Data.Aeson.Types.FromJSON.FromJSON a) => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeful.Collection.ClientStore ci si a)
+ Data.Mergeful.Collection: instance (GHC.Classes.Ord ci, GHC.Classes.Ord si, Data.Aeson.Types.FromJSON.FromJSONKey ci, Data.Aeson.Types.FromJSON.FromJSONKey si, Data.Aeson.Types.FromJSON.FromJSON a) => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeful.Collection.SyncRequest ci si a)
+ Data.Mergeful.Collection: instance (GHC.Classes.Ord si, GHC.Classes.Ord ci) => GHC.Classes.Ord (Data.Mergeful.Collection.Identifier ci si)
+ Data.Mergeful.Collection: instance (GHC.Show.Show ci, GHC.Show.Show a, GHC.Show.Show si) => GHC.Show.Show (Data.Mergeful.Collection.ClientStore ci si a)
+ Data.Mergeful.Collection: instance (GHC.Show.Show ci, GHC.Show.Show a, GHC.Show.Show si) => GHC.Show.Show (Data.Mergeful.Collection.SyncRequest ci si a)
+ Data.Mergeful.Collection: instance (GHC.Show.Show ci, GHC.Show.Show si, GHC.Show.Show a) => GHC.Show.Show (Data.Mergeful.Collection.SyncResponse ci si a)
+ Data.Mergeful.Collection: instance (GHC.Show.Show si, GHC.Show.Show a) => GHC.Show.Show (Data.Mergeful.Collection.ServerStore si a)
+ Data.Mergeful.Collection: instance (GHC.Show.Show si, GHC.Show.Show ci) => GHC.Show.Show (Data.Mergeful.Collection.Identifier ci si)
+ Data.Mergeful.Collection: instance Control.DeepSeq.NFData Data.Mergeful.Collection.ClientId
+ Data.Mergeful.Collection: instance Control.DeepSeq.NFData i => Control.DeepSeq.NFData (Data.Mergeful.Collection.ClientAddition i)
+ Data.Mergeful.Collection: instance Data.Aeson.Types.FromJSON.FromJSON i => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeful.Collection.ClientAddition i)
+ Data.Mergeful.Collection: instance Data.Aeson.Types.ToJSON.ToJSON i => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeful.Collection.ClientAddition i)
+ Data.Mergeful.Collection: instance Data.Validity.Validity i => Data.Validity.Validity (Data.Mergeful.Collection.ClientAddition i)
+ Data.Mergeful.Collection: instance GHC.Classes.Eq i => GHC.Classes.Eq (Data.Mergeful.Collection.ClientAddition i)
+ Data.Mergeful.Collection: instance GHC.Generics.Generic (Data.Mergeful.Collection.ClientAddition i)
+ Data.Mergeful.Collection: instance GHC.Generics.Generic (Data.Mergeful.Collection.ClientStore ci si a)
+ Data.Mergeful.Collection: instance GHC.Generics.Generic (Data.Mergeful.Collection.ClientSyncProcessor ci si a m)
+ Data.Mergeful.Collection: instance GHC.Generics.Generic (Data.Mergeful.Collection.Identifier ci si)
+ Data.Mergeful.Collection: instance GHC.Generics.Generic (Data.Mergeful.Collection.ServerStore si a)
+ Data.Mergeful.Collection: instance GHC.Generics.Generic (Data.Mergeful.Collection.ServerSyncProcessor ci si a m)
+ Data.Mergeful.Collection: instance GHC.Generics.Generic (Data.Mergeful.Collection.SyncRequest ci si a)
+ Data.Mergeful.Collection: instance GHC.Generics.Generic (Data.Mergeful.Collection.SyncResponse ci si a)
+ Data.Mergeful.Collection: instance GHC.Show.Show i => GHC.Show.Show (Data.Mergeful.Collection.ClientAddition i)
+ Data.Mergeful.Collection: mergeClientDeletedConflicts :: (a -> ClientDeletedConflictResolution) -> Map si (Timed a) -> Map si (Timed a)
+ Data.Mergeful.Collection: mergeFromClientStrategy :: ItemMergeStrategy a
+ Data.Mergeful.Collection: mergeFromServerStrategy :: ItemMergeStrategy a
+ Data.Mergeful.Collection: mergeServerDeletedConflicts :: (a -> ServerDeletedConflictResolution) -> Map si (Timed a) -> Set si
+ Data.Mergeful.Collection: mergeSyncResponseCustom :: (Ord si, Monad m) => ItemMergeStrategy a -> ClientSyncProcessor ci si a m -> SyncResponse ci si a -> m ()
+ Data.Mergeful.Collection: mergeSyncResponseFromClient :: (Ord ci, Ord si) => ClientStore ci si a -> SyncResponse ci si a -> ClientStore ci si a
+ Data.Mergeful.Collection: mergeSyncResponseUsingCRDT :: (Ord ci, Ord si) => (a -> a -> a) -> ClientStore ci si a -> SyncResponse ci si a -> ClientStore ci si a
+ Data.Mergeful.Collection: mergeSyncedButChangedConflicts :: forall si a. Ord si => (a -> a -> ChangeConflictResolution a) -> Map si (Timed a) -> Map si (Timed a) -> (Map si (Timed a), Map si (Timed a), Map si (Timed a))
+ Data.Mergeful.Collection: mergeUsingCRDTStrategy :: (a -> a -> a) -> ItemMergeStrategy a
+ Data.Mergeful.Collection: newtype ServerTime
+ Data.Mergeful.Collection: processServerSyncCustom :: forall ci si a m. (Ord si, Monad m) => ServerSyncProcessor ci si a m -> SyncRequest ci si a -> m (SyncResponse ci si a)
+ Data.Mergeful.Item: Delete :: ServerDeletedConflictResolution
+ Data.Mergeful.Item: KeepLocal :: ChangeConflictResolution a
+ Data.Mergeful.Item: KeepLocalChange :: ServerDeletedConflictResolution
+ Data.Mergeful.Item: Merged :: a -> ChangeConflictResolution a
+ Data.Mergeful.Item: StayDeleted :: ClientDeletedConflictResolution
+ Data.Mergeful.Item: TakeRemote :: ChangeConflictResolution a
+ Data.Mergeful.Item: TakeRemoteChange :: ClientDeletedConflictResolution
+ Data.Mergeful.Item: data ChangeConflictResolution a
+ Data.Mergeful.Item: data ClientDeletedConflictResolution
+ Data.Mergeful.Item: data ServerDeletedConflictResolution
+ Data.Mergeful.Item: instance Control.DeepSeq.NFData a => Control.DeepSeq.NFData (Data.Mergeful.Item.ClientItem a)
+ Data.Mergeful.Item: instance Control.DeepSeq.NFData a => Control.DeepSeq.NFData (Data.Mergeful.Item.ItemMergeResult a)
+ Data.Mergeful.Item: instance Control.DeepSeq.NFData a => Control.DeepSeq.NFData (Data.Mergeful.Item.ItemSyncRequest a)
+ Data.Mergeful.Item: instance Control.DeepSeq.NFData a => Control.DeepSeq.NFData (Data.Mergeful.Item.ItemSyncResponse a)
+ Data.Mergeful.Item: instance Control.DeepSeq.NFData a => Control.DeepSeq.NFData (Data.Mergeful.Item.ServerItem a)
+ Data.Mergeful.Item: instance GHC.Classes.Eq Data.Mergeful.Item.ClientDeletedConflictResolution
+ Data.Mergeful.Item: instance GHC.Classes.Eq Data.Mergeful.Item.ServerDeletedConflictResolution
+ Data.Mergeful.Item: instance GHC.Classes.Eq a => GHC.Classes.Eq (Data.Mergeful.Item.ChangeConflictResolution a)
+ Data.Mergeful.Item: instance GHC.Generics.Generic (Data.Mergeful.Item.ChangeConflictResolution a)
+ Data.Mergeful.Item: instance GHC.Generics.Generic Data.Mergeful.Item.ClientDeletedConflictResolution
+ Data.Mergeful.Item: instance GHC.Generics.Generic Data.Mergeful.Item.ServerDeletedConflictResolution
+ Data.Mergeful.Item: instance GHC.Show.Show Data.Mergeful.Item.ClientDeletedConflictResolution
+ Data.Mergeful.Item: instance GHC.Show.Show Data.Mergeful.Item.ServerDeletedConflictResolution
+ Data.Mergeful.Item: instance GHC.Show.Show a => GHC.Show.Show (Data.Mergeful.Item.ChangeConflictResolution a)
+ Data.Mergeful.Item: mergeFromClient :: ClientItem a -> ItemMergeResult a -> ClientItem a
+ Data.Mergeful.Item: mergeFromClientStrategy :: ItemMergeStrategy a
+ Data.Mergeful.Item: mergeFromServerStrategy :: ItemMergeStrategy a
+ Data.Mergeful.Item: mergeItemSyncResponseFromClient :: ClientItem a -> ItemSyncResponse a -> ClientItem a
+ Data.Mergeful.Item: mergeItemSyncResponseFromServer :: ClientItem a -> ItemSyncResponse a -> ClientItem a
+ Data.Mergeful.Item: mergeItemSyncResponseUsingCRDT :: (a -> a -> a) -> ClientItem a -> ItemSyncResponse a -> ClientItem a
+ Data.Mergeful.Item: mergeItemSyncResponseUsingStrategy :: ItemMergeStrategy a -> ClientItem a -> ItemSyncResponse a -> ClientItem a
+ Data.Mergeful.Item: mergeUsingCRDT :: (a -> a -> a) -> ClientItem a -> ItemMergeResult a -> ClientItem a
+ Data.Mergeful.Item: mergeUsingCRDTStrategy :: (a -> a -> a) -> ItemMergeStrategy a
+ Data.Mergeful.Timed: ServerTime :: Word64 -> ServerTime
+ Data.Mergeful.Timed: [unServerTime] :: ServerTime -> Word64
+ Data.Mergeful.Timed: instance Control.DeepSeq.NFData Data.Mergeful.Timed.ServerTime
+ Data.Mergeful.Timed: instance Control.DeepSeq.NFData a => Control.DeepSeq.NFData (Data.Mergeful.Timed.Timed a)
+ Data.Mergeful.Timed: newtype ServerTime
+ Data.Mergeful.Value: instance Control.DeepSeq.NFData Data.Mergeful.Value.ChangedFlag
+ Data.Mergeful.Value: instance Control.DeepSeq.NFData a => Control.DeepSeq.NFData (Data.Mergeful.Value.ClientValue a)
+ Data.Mergeful.Value: instance Control.DeepSeq.NFData a => Control.DeepSeq.NFData (Data.Mergeful.Value.ServerValue a)
+ Data.Mergeful.Value: instance Control.DeepSeq.NFData a => Control.DeepSeq.NFData (Data.Mergeful.Value.ValueMergeResult a)
+ Data.Mergeful.Value: instance Control.DeepSeq.NFData a => Control.DeepSeq.NFData (Data.Mergeful.Value.ValueSyncRequest a)
+ Data.Mergeful.Value: instance Control.DeepSeq.NFData a => Control.DeepSeq.NFData (Data.Mergeful.Value.ValueSyncResponse a)
- Data.Mergeful.Collection: ClientStore :: Map ClientId a -> Map i (Timed a) -> Map i (Timed a) -> Map i ServerTime -> ClientStore i a
+ Data.Mergeful.Collection: ClientStore :: Map ci a -> Map si (Timed a) -> Map si (Timed a) -> Map si ServerTime -> ClientStore ci si a
- Data.Mergeful.Collection: ItemMergeStrategy :: (a -> Timed a -> Timed a) -> (Timed a -> Maybe (Timed a)) -> (a -> Maybe a) -> ItemMergeStrategy a
+ Data.Mergeful.Collection: ItemMergeStrategy :: (a -> a -> ChangeConflictResolution a) -> (a -> ClientDeletedConflictResolution) -> (a -> ServerDeletedConflictResolution) -> ItemMergeStrategy a
- Data.Mergeful.Collection: ServerStore :: Map i (Timed a) -> ServerStore i a
+ Data.Mergeful.Collection: ServerStore :: Map si (Timed a) -> ServerStore si a
- Data.Mergeful.Collection: SyncRequest :: Map ClientId a -> Map i ServerTime -> Map i (Timed a) -> Map i ServerTime -> SyncRequest i a
+ Data.Mergeful.Collection: SyncRequest :: !Map ci a -> !Map si ServerTime -> !Map si (Timed a) -> !Map si ServerTime -> SyncRequest ci si 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: SyncResponse :: !Map ci (ClientAddition si) -> !Map si ServerTime -> !Set si -> !Map si (Timed a) -> !Map si (Timed a) -> !Set si -> !Map si (Timed a) -> !Map si (Timed a) -> !Set si -> SyncResponse ci si a
- Data.Mergeful.Collection: [clientStoreAddedItems] :: ClientStore i a -> Map ClientId a
+ Data.Mergeful.Collection: [clientStoreAddedItems] :: ClientStore ci si a -> Map ci a
- Data.Mergeful.Collection: [clientStoreDeletedItems] :: ClientStore i a -> Map i ServerTime
+ Data.Mergeful.Collection: [clientStoreDeletedItems] :: ClientStore ci si a -> Map si ServerTime
- Data.Mergeful.Collection: [clientStoreSyncedButChangedItems] :: ClientStore i a -> Map i (Timed a)
+ Data.Mergeful.Collection: [clientStoreSyncedButChangedItems] :: ClientStore ci si a -> Map si (Timed a)
- Data.Mergeful.Collection: [clientStoreSyncedItems] :: ClientStore i a -> Map i (Timed a)
+ Data.Mergeful.Collection: [clientStoreSyncedItems] :: ClientStore ci si a -> Map si (Timed a)
- Data.Mergeful.Collection: [itemMergeStrategyMergeChangeConflict] :: ItemMergeStrategy a -> a -> Timed a -> Timed a
+ Data.Mergeful.Collection: [itemMergeStrategyMergeChangeConflict] :: ItemMergeStrategy a -> a -> a -> ChangeConflictResolution a
- Data.Mergeful.Collection: [itemMergeStrategyMergeClientDeletedConflict] :: ItemMergeStrategy a -> Timed a -> Maybe (Timed a)
+ Data.Mergeful.Collection: [itemMergeStrategyMergeClientDeletedConflict] :: ItemMergeStrategy a -> a -> ClientDeletedConflictResolution
- Data.Mergeful.Collection: [itemMergeStrategyMergeServerDeletedConflict] :: ItemMergeStrategy a -> a -> Maybe a
+ Data.Mergeful.Collection: [itemMergeStrategyMergeServerDeletedConflict] :: ItemMergeStrategy a -> a -> ServerDeletedConflictResolution
- Data.Mergeful.Collection: [serverStoreItems] :: ServerStore i a -> Map i (Timed a)
+ Data.Mergeful.Collection: [serverStoreItems] :: ServerStore si a -> Map si (Timed a)
- Data.Mergeful.Collection: [syncRequestDeletedItems] :: SyncRequest i a -> Map i ServerTime
+ Data.Mergeful.Collection: [syncRequestDeletedItems] :: SyncRequest ci si a -> !Map si ServerTime
- Data.Mergeful.Collection: [syncRequestKnownButChangedItems] :: SyncRequest i a -> Map i (Timed a)
+ Data.Mergeful.Collection: [syncRequestKnownButChangedItems] :: SyncRequest ci si a -> !Map si (Timed a)
- Data.Mergeful.Collection: [syncRequestKnownItems] :: SyncRequest i a -> Map i ServerTime
+ Data.Mergeful.Collection: [syncRequestKnownItems] :: SyncRequest ci si a -> !Map si ServerTime
- Data.Mergeful.Collection: [syncRequestNewItems] :: SyncRequest i a -> Map ClientId a
+ Data.Mergeful.Collection: [syncRequestNewItems] :: SyncRequest ci si a -> !Map ci a
- Data.Mergeful.Collection: [syncResponseClientAdded] :: SyncResponse i a -> Map ClientId (i, ServerTime)
+ Data.Mergeful.Collection: [syncResponseClientAdded] :: SyncResponse ci si a -> !Map ci (ClientAddition si)
- Data.Mergeful.Collection: [syncResponseClientChanged] :: SyncResponse i a -> Map i ServerTime
+ Data.Mergeful.Collection: [syncResponseClientChanged] :: SyncResponse ci si a -> !Map si ServerTime
- Data.Mergeful.Collection: [syncResponseClientDeleted] :: SyncResponse i a -> Set i
+ Data.Mergeful.Collection: [syncResponseClientDeleted] :: SyncResponse ci si a -> !Set si
- Data.Mergeful.Collection: [syncResponseConflictsClientDeleted] :: SyncResponse i a -> Map i (Timed a)
+ Data.Mergeful.Collection: [syncResponseConflictsClientDeleted] :: SyncResponse ci si a -> !Map si (Timed a)
- Data.Mergeful.Collection: [syncResponseConflictsServerDeleted] :: SyncResponse i a -> Set i
+ Data.Mergeful.Collection: [syncResponseConflictsServerDeleted] :: SyncResponse ci si a -> !Set si
- Data.Mergeful.Collection: [syncResponseConflicts] :: SyncResponse i a -> Map i (Timed a)
+ Data.Mergeful.Collection: [syncResponseConflicts] :: SyncResponse ci si a -> !Map si (Timed a)
- Data.Mergeful.Collection: [syncResponseServerAdded] :: SyncResponse i a -> Map i (Timed a)
+ Data.Mergeful.Collection: [syncResponseServerAdded] :: SyncResponse ci si a -> !Map si (Timed a)
- Data.Mergeful.Collection: [syncResponseServerChanged] :: SyncResponse i a -> Map i (Timed a)
+ Data.Mergeful.Collection: [syncResponseServerChanged] :: SyncResponse ci si a -> !Map si (Timed a)
- Data.Mergeful.Collection: [syncResponseServerDeleted] :: SyncResponse i a -> Set i
+ Data.Mergeful.Collection: [syncResponseServerDeleted] :: SyncResponse ci si a -> !Set si
- Data.Mergeful.Collection: addItemToClientStore :: a -> ClientStore i a -> ClientStore i a
+ Data.Mergeful.Collection: addItemToClientStore :: (Ord ci, Enum ci, Bounded ci) => a -> ClientStore ci si a -> ClientStore ci si a
- Data.Mergeful.Collection: changeItemInClientStore :: Ord i => i -> a -> ClientStore i a -> ClientStore i a
+ Data.Mergeful.Collection: changeItemInClientStore :: Ord si => si -> a -> ClientStore ci si a -> ClientStore ci si a
- Data.Mergeful.Collection: clientStoreClientIdSet :: ClientStore i a -> Set ClientId
+ Data.Mergeful.Collection: clientStoreClientIdSet :: ClientStore ci si a -> Set ci
- Data.Mergeful.Collection: clientStoreItems :: Ord i => ClientStore i a -> Map (Either ClientId i) a
+ Data.Mergeful.Collection: clientStoreItems :: (Ord ci, Ord si) => ClientStore ci si a -> Map (Either ci si) a
- Data.Mergeful.Collection: clientStoreSize :: ClientStore i a -> Word
+ Data.Mergeful.Collection: clientStoreSize :: ClientStore ci si a -> Word
- Data.Mergeful.Collection: clientStoreSyncIdSet :: Ord i => ClientStore i a -> Set i
+ Data.Mergeful.Collection: clientStoreSyncIdSet :: Ord si => ClientStore ci si a -> Set si
- Data.Mergeful.Collection: clientStoreUndeletedSyncIdSet :: Ord i => ClientStore i a -> Set i
+ Data.Mergeful.Collection: clientStoreUndeletedSyncIdSet :: Ord si => ClientStore ci si a -> Set si
- Data.Mergeful.Collection: data ClientStore i a
+ Data.Mergeful.Collection: data ClientStore ci si a
- Data.Mergeful.Collection: data SyncRequest i a
+ Data.Mergeful.Collection: data SyncRequest ci si a
- Data.Mergeful.Collection: data SyncResponse i a
+ Data.Mergeful.Collection: data SyncResponse ci si a
- Data.Mergeful.Collection: deleteItemFromClientStore :: ClientId -> ClientStore i a -> ClientStore i a
+ Data.Mergeful.Collection: deleteItemFromClientStore :: Ord ci => ci -> ClientStore ci si a -> ClientStore ci si a
- Data.Mergeful.Collection: emptySyncResponse :: SyncResponse i a
+ Data.Mergeful.Collection: emptySyncResponse :: SyncResponse ci si a
- Data.Mergeful.Collection: initialClientStore :: ClientStore i a
+ Data.Mergeful.Collection: initialClientStore :: ClientStore ci si a
- Data.Mergeful.Collection: initialServerStore :: ServerStore i a
+ Data.Mergeful.Collection: initialServerStore :: ServerStore si a
- Data.Mergeful.Collection: initialSyncRequest :: SyncRequest i a
+ Data.Mergeful.Collection: initialSyncRequest :: SyncRequest ci si a
- Data.Mergeful.Collection: makeSyncRequest :: ClientStore i a -> SyncRequest i a
+ Data.Mergeful.Collection: makeSyncRequest :: ClientStore ci si a -> SyncRequest ci si a
- Data.Mergeful.Collection: markItemDeletedInClientStore :: Ord i => i -> ClientStore i a -> ClientStore i a
+ Data.Mergeful.Collection: markItemDeletedInClientStore :: Ord si => si -> ClientStore ci si a -> ClientStore ci si 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: mergeAddedItems :: forall ci si a. (Ord ci, Ord si) => Map ci a -> Map ci (ClientAddition si) -> (Map ci a, Map si (Timed a))
- Data.Mergeful.Collection: mergeSyncResponseFromServer :: Ord i => ClientStore i a -> SyncResponse i a -> ClientStore i a
+ Data.Mergeful.Collection: mergeSyncResponseFromServer :: (Ord ci, Ord si) => ClientStore ci si a -> SyncResponse ci si a -> ClientStore ci si a
- Data.Mergeful.Collection: mergeSyncResponseUsingStrategy :: Ord i => ItemMergeStrategy a -> ClientStore i a -> SyncResponse i a -> ClientStore i a
+ Data.Mergeful.Collection: mergeSyncResponseUsingStrategy :: (Ord ci, Ord si) => ItemMergeStrategy a -> ClientStore ci si a -> SyncResponse ci si a -> ClientStore ci si a
- Data.Mergeful.Collection: newtype ServerStore i a
+ Data.Mergeful.Collection: newtype ServerStore si 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.Collection: processServerSync :: forall ci si a m. (Ord si, Monad m) => m si -> ServerStore si a -> SyncRequest ci si a -> m (SyncResponse ci si a, ServerStore si a)
- Data.Mergeful.Item: ItemMergeStrategy :: (a -> Timed a -> Timed a) -> (Timed a -> Maybe (Timed a)) -> (a -> Maybe a) -> ItemMergeStrategy a
+ Data.Mergeful.Item: ItemMergeStrategy :: (a -> a -> ChangeConflictResolution a) -> (a -> ClientDeletedConflictResolution) -> (a -> ServerDeletedConflictResolution) -> ItemMergeStrategy a
- Data.Mergeful.Item: [itemMergeStrategyMergeChangeConflict] :: ItemMergeStrategy a -> a -> Timed a -> Timed a
+ Data.Mergeful.Item: [itemMergeStrategyMergeChangeConflict] :: ItemMergeStrategy a -> a -> a -> ChangeConflictResolution a
- Data.Mergeful.Item: [itemMergeStrategyMergeClientDeletedConflict] :: ItemMergeStrategy a -> Timed a -> Maybe (Timed a)
+ Data.Mergeful.Item: [itemMergeStrategyMergeClientDeletedConflict] :: ItemMergeStrategy a -> a -> ClientDeletedConflictResolution
- Data.Mergeful.Item: [itemMergeStrategyMergeServerDeletedConflict] :: ItemMergeStrategy a -> a -> Maybe a
+ Data.Mergeful.Item: [itemMergeStrategyMergeServerDeletedConflict] :: ItemMergeStrategy a -> a -> ServerDeletedConflictResolution
Files
- CHANGELOG.md +13/−0
- mergeful.cabal +6/−4
- src/Data/Mergeful.hs +3/−2
- src/Data/Mergeful/Collection.hs +964/−755
- src/Data/Mergeful/Item.hs +315/−239
- src/Data/Mergeful/Timed.hs +29/−18
- src/Data/Mergeful/Value.hs +120/−105
CHANGELOG.md view
@@ -2,6 +2,19 @@ ## [Unreleased] +## [0.2.0.0] - 2020-05-21++## Added++* processServerSyncCustom and related+* mergeSyncResponseCustom and related++### Changed++* Another parameter: the client id so that you can use an sql id as the client id.+ This likely means that none of your code that works with mergeful-0.1 will compile anymore+ but it is easy to fix by adding the right parameter.+ ## [0.1.0.0] - 2019-09-23 ### Changed
mergeful.cabal view
@@ -1,19 +1,19 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.31.2.+-- This file has been generated from package.yaml by hpack version 0.33.0. -- -- see: https://github.com/sol/hpack ----- hash: 165daa0156c2d4d4295cd5dbfa2b75914c44985aca486dca68525510afa932ce+-- hash: 5c04518a45f3748d42c829e645b69f607f23450b79fa96bcd6c567976b0465b7 name: mergeful-version: 0.1.0.0+version: 0.2.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 author: Tom Sydney Kerckhove maintainer: syd@cs-syd.eu-copyright: Copyright: (c) 2019 Tom Sydney Kerckhove+copyright: Copyright: (c) 2019-2020 Tom Sydney Kerckhove license: MIT license-file: LICENSE build-type: Simple@@ -36,10 +36,12 @@ Paths_mergeful hs-source-dirs: src+ ghc-options: -Wall -fwarn-redundant-constraints build-depends: aeson , base >=4.7 && <5 , containers+ , deepseq , mtl , text , time
src/Data/Mergeful.hs view
@@ -1,5 +1,6 @@ module Data.Mergeful- ( module Data.Mergeful.Collection- ) where+ ( module Data.Mergeful.Collection,+ )+where import Data.Mergeful.Collection
src/Data/Mergeful/Collection.hs view
@@ -1,757 +1,966 @@ {-# 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')+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE KindSignatures #-}+{-# 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' or 'processServerSyncCustom'.+-- * 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+ ( -- * Client side+ ClientStore (..),+ Timed (..),+ ServerTime (..),+ initialClientStore,++ -- ** Querying the client store+ clientStoreSize,+ clientStoreClientIdSet,+ clientStoreUndeletedSyncIdSet,+ clientStoreSyncIdSet,+ clientStoreItems,++ -- ** Changing the client store+ addItemToClientStore,+ findFreeSpot,+ markItemDeletedInClientStore,+ changeItemInClientStore,+ deleteItemFromClientStore,++ -- ** Maxing a sync request+ SyncRequest (..),+ initialSyncRequest,+ makeSyncRequest,++ -- ** Merging the response+ SyncResponse (..),+ ClientAddition (..),+ ItemMergeStrategy (..),+ ChangeConflictResolution (..),+ ClientDeletedConflictResolution (..),+ ServerDeletedConflictResolution (..),+ mergeFromServerStrategy,+ mergeFromClientStrategy,+ mergeUsingCRDTStrategy,+ mergeSyncResponseFromServer,+ mergeSyncResponseFromClient,+ mergeSyncResponseUsingCRDT,+ mergeSyncResponseUsingStrategy,+ ClientSyncProcessor (..),+ mergeSyncResponseCustom,++ -- *** Utility functions for implementing pure client-side merging+ ClientId (..),+ mergeAddedItems,+ mergeSyncedButChangedItems,+ mergeDeletedItems,++ -- *** Utility functions for implementing custom client-side merging+ mergeSyncedButChangedConflicts,+ mergeClientDeletedConflicts,+ mergeServerDeletedConflicts,++ -- * Server side++ -- ** The store+ ServerStore (..),+ initialServerStore,++ -- ** Processing a sync request+ processServerSync,+ ServerSyncProcessor (..),+ processServerSyncCustom,+ emptySyncResponse,+ initialServerTime,+ incrementServerTime,+ )+where++import Control.Applicative+import Control.DeepSeq+import Control.Monad+import Control.Monad.State+import Data.Aeson+import Data.List+import Data.Map (Map)+import qualified Data.Map as M+import Data.Maybe+import Data.Mergeful.Item+import Data.Mergeful.Timed+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 GHC.Generics (Generic)++-- | 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++data ClientStore ci si a+ = ClientStore+ { -- | These items are new locally but have not been synced to the server yet.+ clientStoreAddedItems :: Map ci a,+ -- | These items have been synced at their respective 'ServerTime's.+ clientStoreSyncedItems :: Map si (Timed a),+ -- | These items have been synced at their respective 'ServerTime's+ -- but modified locally since then.+ clientStoreSyncedButChangedItems :: Map si (Timed a),+ -- | These items have been deleted locally after they were synced+ -- but the server has not been notified of that yet.+ clientStoreDeletedItems :: Map si ServerTime+ }+ deriving (Show, Eq, Generic)++instance+ (Validity ci, Validity si, Show ci, Show si, Ord ci, Ord si, Validity a) =>+ Validity (ClientStore ci si 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 (NFData ci, NFData si, NFData a) => NFData (ClientStore ci si a)++instance+ (Ord ci, Ord si, FromJSONKey ci, FromJSONKey si, FromJSON a) =>+ FromJSON (ClientStore ci si 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 ci, ToJSONKey si, ToJSON a) => ToJSON (ClientStore ci si 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 ci si 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 ci si 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 ci si a -> Set ci+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 si => ClientStore ci si a -> Set si+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 si => ClientStore ci si a -> Set si+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 ci, Ord si) => ClientStore ci si a -> Map (Either ci si) 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 'ci's in the map.+addItemToClientStore ::+ (Ord ci, Enum ci, Bounded ci) => a -> ClientStore ci si a -> ClientStore ci si a+addItemToClientStore a cs =+ let oldAddedItems = clientStoreAddedItems cs+ newAddedItems =+ let newKey = findFreeSpot oldAddedItems+ in M.insert newKey a oldAddedItems+ in cs {clientStoreAddedItems = newAddedItems}++-- | Find a free client id to use+--+-- You shouldn't need this function, 'addItemToClientStore' takes care of this.+--+-- The values wrap around when reaching 'maxBound'.+findFreeSpot :: (Ord ci, Enum ci, Bounded ci) => Map ci a -> ci+findFreeSpot m =+ if M.null m+ then minBound+ 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 ci+ | ci == maxBound = minBound+ | otherwise = succ ci++-- | Mark an item deleted in a client store.+--+-- This function will not delete the item, but mark it as deleted instead.+markItemDeletedInClientStore :: Ord si => si -> ClientStore ci si a -> ClientStore ci si 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 si => si -> a -> ClientStore ci si a -> ClientStore ci si 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 :: Ord ci => ci -> ClientStore ci si a -> ClientStore ci si a+deleteItemFromClientStore i cs = cs {clientStoreAddedItems = M.delete i (clientStoreAddedItems cs)}++newtype ServerStore si a+ = ServerStore+ { -- | A map of items, named using an 'si', together with the 'ServerTime' at which+ -- they were last synced.+ serverStoreItems :: Map si (Timed a)+ }+ deriving (Show, Eq, Generic, FromJSON, ToJSON)++instance (Validity si, Show si, Ord si, Validity a) => Validity (ServerStore si a)++instance (NFData si, NFData a) => NFData (ServerStore si a)++-- | A server store to start with+--+-- This store contains no items.+initialServerStore :: ServerStore si a+initialServerStore = ServerStore {serverStoreItems = M.empty}++data SyncRequest ci si a+ = SyncRequest+ { -- | These items are new locally but have not been synced to the server yet.+ syncRequestNewItems :: !(Map ci a),+ -- | These items have been synced at their respective 'ServerTime's.+ syncRequestKnownItems :: !(Map si ServerTime),+ -- | These items have been synced at their respective 'ServerTime's+ -- but modified locally since then.+ syncRequestKnownButChangedItems :: !(Map si (Timed a)),+ -- | These items have been deleted locally after they were synced+ -- but the server has not been notified of that yet.+ syncRequestDeletedItems :: !(Map si ServerTime)+ }+ deriving (Show, Eq, Generic)++instance+ (Validity ci, Validity si, Show ci, Show si, Ord ci, Ord si, Validity a) =>+ Validity (SyncRequest ci si 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 (NFData ci, NFData si, NFData a) => NFData (SyncRequest ci si a)++instance+ (Ord ci, Ord si, FromJSONKey ci, FromJSONKey si, FromJSON a) =>+ FromJSON (SyncRequest ci si a)+ where+ parseJSON =+ withObject "SyncRequest" $ \o ->+ SyncRequest <$> o .:? "added" .!= M.empty <*> o .:? "synced" .!= M.empty+ <*> o .:? "changed" .!= M.empty+ <*> o .:? "deleted" .!= M.empty++instance (ToJSONKey ci, ToJSONKey si, ToJSON a) => ToJSON (SyncRequest ci si a) where+ toJSON SyncRequest {..} =+ object $+ catMaybes+ [ jNull "added" 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 ci si a+initialSyncRequest =+ SyncRequest+ { syncRequestNewItems = M.empty,+ syncRequestKnownItems = M.empty,+ syncRequestKnownButChangedItems = M.empty,+ syncRequestDeletedItems = M.empty+ }++data ClientAddition i+ = ClientAddition+ { clientAdditionId :: i,+ clientAdditionServerTime :: ServerTime+ }+ deriving (Show, Eq, Generic)++instance Validity i => Validity (ClientAddition i)++instance NFData i => NFData (ClientAddition i)++instance FromJSON i => FromJSON (ClientAddition i) where+ parseJSON = withObject "ClientAddition" $ \o -> ClientAddition <$> o .: "id" <*> o .: "time"++instance ToJSON i => ToJSON (ClientAddition i) where+ toJSON ClientAddition {..} = object ["id" .= clientAdditionId, "time" .= clientAdditionServerTime]++data SyncResponse ci si a+ = SyncResponse+ { -- | The client added these items and server has succesfully been made aware of that.+ --+ -- The client needs to update their server times+ syncResponseClientAdded :: !(Map ci (ClientAddition si)),+ -- | The client changed these items and server has succesfully been made aware of that.+ --+ -- The client needs to update their server times+ syncResponseClientChanged :: !(Map si ServerTime),+ -- | The client deleted these items and server has succesfully been made aware of that.+ --+ -- The client can delete them from its deleted items+ syncResponseClientDeleted :: !(Set si),+ -- | These items have been added on the server side+ --+ -- The client should add them too.+ syncResponseServerAdded :: !(Map si (Timed a)),+ -- | These items have been modified on the server side.+ --+ -- The client should modify them too.+ syncResponseServerChanged :: !(Map si (Timed a)),+ -- | These items were deleted on the server side+ --+ -- The client should delete them too+ syncResponseServerDeleted :: !(Set si),+ -- | 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.+ syncResponseConflicts :: !(Map si (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.+ syncResponseConflictsClientDeleted :: !(Map si (Timed a)),+ -- | 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.+ syncResponseConflictsServerDeleted :: !(Set si)+ }+ deriving (Show, Eq, Generic)++instance+ (Validity ci, Validity si, Show ci, Show si, Ord ci, Ord si, Validity a) =>+ Validity (SyncResponse ci si a)+ where+ validate sr@SyncResponse {..} =+ mconcat+ [ genericValidate sr,+ declare "There are no duplicate IDs"+ $ distinct+ $ concat+ [ map (\(_, ClientAddition {..}) -> clientAdditionId) $ 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 (NFData ci, NFData si, NFData a) => NFData (SyncResponse ci si a)++instance+ (Ord ci, Ord si, FromJSON ci, FromJSON si, FromJSONKey ci, FromJSONKey si, FromJSON a) =>+ FromJSON (SyncResponse ci si 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 ci, ToJSON si, ToJSONKey ci, ToJSONKey si, ToJSON a) =>+ ToJSON (SyncResponse ci si 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+ ]++-- | A sync response to start with.+--+-- It is entirely empty.+emptySyncResponse :: SyncResponse ci si 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 ci si a -> SyncRequest ci si a+makeSyncRequest ClientStore {..} =+ SyncRequest+ { syncRequestNewItems = clientStoreAddedItems,+ syncRequestKnownItems = M.map timedTime clientStoreSyncedItems,+ syncRequestKnownButChangedItems = clientStoreSyncedButChangedItems,+ syncRequestDeletedItems = clientStoreDeletedItems+ }++-- | Merge a 'SyncResponse' into the current 'ClientStore' by taking whatever the server gave the client in case of conflict.+--+-- Pro: Clients will converge on the same value.+--+-- __Con: Conflicting updates will be lost.__+mergeSyncResponseFromServer ::+ (Ord ci, Ord si) => ClientStore ci si a -> SyncResponse ci si a -> ClientStore ci si a+mergeSyncResponseFromServer =+ mergeSyncResponseUsingStrategy mergeFromServerStrategy++-- | Merge a 'SyncResponse' into the current 'ClientStore' by keeping whatever the client had in case of conflict.+--+-- Pro: No data will be lost+--+-- __Con: Clients will diverge when conflicts occur.__+mergeSyncResponseFromClient ::+ (Ord ci, Ord si) => ClientStore ci si a -> SyncResponse ci si a -> ClientStore ci si a+mergeSyncResponseFromClient = mergeSyncResponseUsingStrategy mergeFromClientStrategy++-- | Merge a 'SyncResponse' into the current 'ClientStore' by using the given GADT merging function in case of conflict+mergeSyncResponseUsingCRDT :: (Ord ci, Ord si) => (a -> a -> a) -> ClientStore ci si a -> SyncResponse ci si a -> ClientStore ci si a+mergeSyncResponseUsingCRDT = mergeSyncResponseUsingStrategy . mergeUsingCRDTStrategy++-- | 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 ci, Ord si) =>+ ItemMergeStrategy a ->+ ClientStore ci si a ->+ SyncResponse ci si a ->+ ClientStore ci si a+mergeSyncResponseUsingStrategy strat cs sr =+ flip execState cs $ mergeSyncResponseCustom strat pureClientSyncProcessor sr++pureClientSyncProcessor :: forall ci si a. (Ord ci, Ord si) => ClientSyncProcessor ci si a (State (ClientStore ci si a))+pureClientSyncProcessor =+ ClientSyncProcessor+ { clientSyncProcessorQuerySyncedButChangedValues = \s ->+ gets+ ( \cs -> M.intersection (clientStoreSyncedButChangedItems cs) (M.fromSet (const ()) s)+ ),+ clientSyncProcessorSyncClientAdded = \m ->+ modify+ ( \cs ->+ let (leftovers, added) = mergeAddedItems (clientStoreAddedItems cs) m+ in cs {clientStoreAddedItems = leftovers, clientStoreSyncedItems = added `M.union` clientStoreSyncedItems cs}+ ),+ clientSyncProcessorSyncClientChanged = \m ->+ modify+ ( \cs ->+ let (leftovers, changed) = mergeSyncedButChangedItems (clientStoreSyncedButChangedItems cs) m+ in cs {clientStoreSyncedButChangedItems = leftovers, clientStoreSyncedItems = changed `M.union` clientStoreSyncedItems cs}+ ),+ clientSyncProcessorSyncClientDeleted = \s ->+ modify+ ( \cs ->+ let leftovers = mergeDeletedItems (clientStoreDeletedItems cs) s+ in cs {clientStoreDeletedItems = leftovers}+ ),+ clientSyncProcessorSyncMergedConflict = \resolved ->+ modify+ ( \cs ->+ let newSyncedButChanged = M.union resolved (clientStoreSyncedButChangedItems cs)+ in cs {clientStoreSyncedButChangedItems = newSyncedButChanged, clientStoreSyncedItems = clientStoreSyncedItems cs `M.difference` newSyncedButChanged}+ ),+ clientSyncProcessorSyncServerAdded = \m ->+ modify (\cs -> cs {clientStoreSyncedItems = m `M.union` clientStoreSyncedItems cs}),+ clientSyncProcessorSyncServerChanged = \m ->+ modify+ ( \cs ->+ let newSynced = m `M.union` clientStoreSyncedItems cs+ in cs {clientStoreSyncedItems = newSynced, clientStoreSyncedButChangedItems = clientStoreSyncedButChangedItems cs `M.difference` newSynced}+ ),+ clientSyncProcessorSyncServerDeleted = \s ->+ modify+ ( \cs ->+ let m = M.fromSet (const ()) s+ in cs+ { clientStoreSyncedItems = clientStoreSyncedItems cs `M.difference` m,+ clientStoreSyncedButChangedItems = clientStoreSyncedButChangedItems cs `M.difference` m+ }+ )+ }++-- | Merge the local added items with the ones that the server has acknowledged as added.+mergeAddedItems ::+ forall ci si a.+ (Ord ci, Ord si) =>+ Map ci a ->+ Map ci (ClientAddition si) ->+ (Map ci a, Map si (Timed a))+mergeAddedItems local added = M.foldlWithKey go (M.empty, M.empty) local+ where+ go :: (Map ci a, Map si (Timed a)) -> ci -> a -> (Map ci a, Map si (Timed a))+ go (as, m) ci a =+ case M.lookup ci added of+ Nothing -> (M.insert ci a as, m)+ Just ClientAddition {..} ->+ ( as,+ M.insert+ clientAdditionId+ (Timed {timedValue = a, timedTime = clientAdditionServerTime})+ 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 ClientSyncProcessor ci si a (m :: * -> *)+ = ClientSyncProcessor+ { -- | Get the synced values with keys in the given set+ clientSyncProcessorQuerySyncedButChangedValues :: !(Set si -> m (Map si (Timed a))),+ -- | Complete additions that were acknowledged by the server.+ -- This involves saving the server id and the server time+ clientSyncProcessorSyncClientAdded :: !(Map ci (ClientAddition si) -> m ()),+ -- | Complete changes that were acknowledged by the server+ -- This involves updating the server time+ clientSyncProcessorSyncClientChanged :: !(Map si ServerTime -> m ()),+ -- | Complete deletions that were acknowledged by the server+ -- This means deleting these tombstoned items entirely+ clientSyncProcessorSyncClientDeleted :: !(Set si -> m ()),+ -- | Store the items that were in a conflict but the conflict was resolved correctly.+ -- These items should be marked as changed.+ clientSyncProcessorSyncMergedConflict :: !(Map si (Timed a) -> m ()),+ clientSyncProcessorSyncServerAdded :: !(Map si (Timed a) -> m ()),+ clientSyncProcessorSyncServerChanged :: !(Map si (Timed a) -> m ()),+ clientSyncProcessorSyncServerDeleted :: !(Set si -> m ())+ }+ deriving (Generic)++mergeSyncResponseCustom :: (Ord si, Monad m) => ItemMergeStrategy a -> ClientSyncProcessor ci si a m -> SyncResponse ci si a -> m ()+mergeSyncResponseCustom ItemMergeStrategy {..} ClientSyncProcessor {..} SyncResponse {..} = do+ -- Every client deleted conflict needs to be added, if the sync processor says so+ let resolvedClientDeletedConflicts = mergeClientDeletedConflicts itemMergeStrategyMergeClientDeletedConflict syncResponseConflictsClientDeleted+ -- Every change conflict, unless the client item is kept, needs to be updated+ -- The unresolved conflicts don't need to be updated.+ clientChangeConflicts <- clientSyncProcessorQuerySyncedButChangedValues $ M.keysSet syncResponseConflicts+ let (_, mergedChangeConflicts, resolvedChangeConflicts) = mergeSyncedButChangedConflicts itemMergeStrategyMergeChangeConflict clientChangeConflicts syncResponseConflicts+ -- Every served deleted conflict needs to be deleted, if the sync processor says so+ clientServerDeletedConflicts <- clientSyncProcessorQuerySyncedButChangedValues syncResponseConflictsServerDeleted+ let resolvedServerDeletedConflicts = mergeServerDeletedConflicts itemMergeStrategyMergeServerDeletedConflict clientServerDeletedConflicts+ -- The order here matters.+ clientSyncProcessorSyncServerAdded $ M.union syncResponseServerAdded resolvedClientDeletedConflicts+ clientSyncProcessorSyncServerChanged $ M.union syncResponseServerChanged resolvedChangeConflicts+ clientSyncProcessorSyncServerDeleted $ S.union syncResponseServerDeleted resolvedServerDeletedConflicts+ clientSyncProcessorSyncMergedConflict mergedChangeConflicts+ clientSyncProcessorSyncClientDeleted syncResponseClientDeleted+ clientSyncProcessorSyncClientChanged syncResponseClientChanged+ clientSyncProcessorSyncClientAdded syncResponseClientAdded++-- | Resolve change conflicts+mergeSyncedButChangedConflicts ::+ forall si a.+ Ord si =>+ (a -> a -> ChangeConflictResolution a) ->+ -- | The conflicting items on the client side+ Map si (Timed a) ->+ -- | The conflicting items on the server side+ Map si (Timed a) ->+ -- | Unresolved conflicts on the left, merged conflicts in the middle, resolved conflicts on the right+ --+ -- * The unresolved conflicts should remain as-is+ -- * The merged conflicts should be updated and marked as changed+ -- * The resolved conflicts should be updated and marked as unchanged+ (Map si (Timed a), Map si (Timed a), Map si (Timed a))+mergeSyncedButChangedConflicts func clientItems =+ M.foldlWithKey go (M.empty, M.empty, M.empty)+ where+ go ::+ (Map si (Timed a), Map si (Timed a), Map si (Timed a)) ->+ si ->+ Timed a ->+ (Map si (Timed a), Map si (Timed a), Map si (Timed a))+ go tup@(unresolved, merged, resolved) key s@(Timed si st) = case M.lookup key clientItems of+ Nothing -> tup -- TODO not even sure what this would mean. Should not happen I guess. Just throw it away+ Just c@(Timed ci _) -> case func ci si of+ KeepLocal ->+ (M.insert key c unresolved, merged, resolved)+ Merged mi ->+ (unresolved, M.insert key (Timed mi st) merged, M.insert key s resolved)+ TakeRemote ->+ (unresolved, merged, M.insert key s resolved)++-- | Resolve client deleted conflicts+mergeClientDeletedConflicts ::+ (a -> ClientDeletedConflictResolution) ->+ -- | The conflicting items on the server side+ Map si (Timed a) ->+ -- | A map of items that need to be updated on the client.+ Map si (Timed a)+mergeClientDeletedConflicts func = M.filter $ \(Timed si _) ->+ case func si of+ TakeRemoteChange -> True+ StayDeleted -> False++-- | Resolve server deleted conflicts+mergeServerDeletedConflicts ::+ (a -> ServerDeletedConflictResolution) ->+ -- | The conflicting items on the client side+ Map si (Timed a) ->+ -- | The result is a map of items that need to be deleted on the client.+ Set si+mergeServerDeletedConflicts func m = M.keysSet $ flip M.filter m $ \(Timed si _) -> case func si of+ KeepLocalChange -> False+ Delete -> True++data Identifier ci si+ = OnlyServer si+ | BothServerAndClient si ci+ deriving (Show, Eq, Ord, Generic)++instance (Validity ci, Validity si) => Validity (Identifier ci si)++instance (NFData ci, NFData si) => NFData (Identifier ci si)++data ServerSyncProcessor ci si a m+ = ServerSyncProcessor+ { -- | Read all items+ serverSyncProcessorRead :: !(m (Map si (Timed a))),+ -- | Add an item with 'initialServerTime'+ serverSyncProcessorAddItem :: !(a -> m si),+ -- | Update an item+ serverSyncProcessorChangeItem :: !(si -> ServerTime -> a -> m ()),+ -- | Delete an item+ serverSyncProcessorDeleteItem :: !(si -> m ())+ }+ deriving (Generic)++-- | Process a server sync+--+-- === __Implementation Details__+--+-- There are four cases for the items in the sync request+--+-- - Added (A)+-- - Synced (S)+-- - Changed (C)+-- - Deleted (D)+--+-- Each of them present options and may require action on the sever side:+--+-- * Added:+--+-- * Client Added (CA) (This is the only case where a new identifier needs to be generated.)+--+-- * Synced:+--+-- * Server Changed (SC) (Nothing)+-- * Server Deleted (SD) (Nothing)+--+-- * Changed:+--+-- * Client Changed (CC) (Update value and increment server time)+-- * Change Conflict (CConf) (Nothing)+-- * Server Deleted Conflict (SDC) (Nothing)+--+-- * Deleted:+--+-- * Client Deleted (CD) (Delete the item)+-- * Client Deleted Conflict (CDC) (Nothing)+--+-- * Extra:+--+-- * Server Added (SA) (Nothing)+--+-- For more detailed comments of the nine cases, see the source of 'processServerItemSync' in the "Data.Mergeful.Item".+processServerSyncCustom ::+ forall ci si a m.+ ( Ord si,+ Monad m+ ) =>+ -- | Your server sync processor+ ServerSyncProcessor ci si a m ->+ SyncRequest ci si a ->+ m (SyncResponse ci si a)+processServerSyncCustom ServerSyncProcessor {..} SyncRequest {..} = do+ serverItems <- serverSyncProcessorRead+ -- A: CA (generate a new identifier)+ syncResponseClientAdded <- forM syncRequestNewItems $ \a -> do+ si <- serverSyncProcessorAddItem a+ pure $ ClientAddition {clientAdditionId = si, clientAdditionServerTime = initialServerTime}+ -- C:+ let decideOnSynced tup@(sc, sd) (si, ct) =+ case M.lookup si serverItems of+ -- SD: The server must have deleted it.+ Nothing -> (sc, S.insert si sd)+ Just t@(Timed _ st) ->+ if ct >= st+ then tup -- In sync+ else (M.insert si t sc, sd) -- SC: The server has changed it because its server time is newer+ let (syncResponseServerChanged, syncResponseServerDeleted) = foldl decideOnSynced (M.empty, S.empty) (M.toList syncRequestKnownItems)+ -- S:+ let decideOnChanged (cc, cConf, sdc) (si, Timed clientItem ct) = do+ case M.lookup si serverItems of+ -- SDC+ Nothing -> pure (cc, cConf, S.insert si sdc)+ Just serverTimed@(Timed _ st) ->+ if ct >= st+ then do+ -- CC+ let st' = incrementServerTime st+ -- Update the server item+ serverSyncProcessorChangeItem si st' clientItem+ pure (M.insert si st' cc, cConf, sdc)+ else do+ -- CConf+ pure (cc, M.insert si serverTimed cConf, sdc)+ (syncResponseClientChanged, syncResponseConflicts, syncResponseConflictsServerDeleted) <- foldM decideOnChanged (M.empty, M.empty, S.empty) (M.toList syncRequestKnownButChangedItems)+ --- D:+ let decideOnDeleted (cd, cdc) (si, ct) = do+ case M.lookup si serverItems of+ Nothing -> do+ -- CD: It was already deleted on the server side, Just pretend that the client made that happen.+ pure (S.insert si cd, cdc)+ Just serverTimed@(Timed _ st) ->+ if ct >= st+ then do+ -- CD+ -- Delete the item+ serverSyncProcessorDeleteItem si+ pure (S.insert si cd, cdc)+ else do+ -- CDC+ pure (cd, M.insert si serverTimed cdc)+ (syncResponseClientDeleted, syncResponseConflictsClientDeleted) <- foldM decideOnDeleted (S.empty, M.empty) (M.toList syncRequestDeletedItems)+ -- Extra: for all items that are in the server but not in the sync request, we need to say they are server added.+ let syncResponseServerAdded = serverItems `M.difference` M.unions [() <$ syncRequestKnownItems, () <$ syncRequestKnownButChangedItems, () <$ syncRequestDeletedItems]+ pure SyncResponse {..}++-- | Serve an 'SyncRequest' using the current 'ServerStore', producing an 'SyncResponse' and a new 'ServerStore'.+processServerSync ::+ forall ci si a m.+ ( Ord si,+ Monad m+ ) =>+ -- | The action that is guaranteed to generate unique identifiers+ m si ->+ ServerStore si a ->+ SyncRequest ci si a ->+ m (SyncResponse ci si a, ServerStore si a)+processServerSync genId ss sr = runStateT (processServerSyncCustom (pureServerSyncProcessor genId) sr) ss++-- | A potentially pure sync processor+pureServerSyncProcessor ::+ (Ord si, Monad m) =>+ -- | The action that is guaranteed to generate unique identifiers+ m si ->+ ServerSyncProcessor ci si a (StateT (ServerStore si a) m)+pureServerSyncProcessor genId = ServerSyncProcessor {..}+ where+ serverSyncProcessorRead = gets serverStoreItems+ serverSyncProcessorAddItem a = do+ i <- lift genId+ modify (\(ServerStore m) -> ServerStore (M.insert i (Timed a initialServerTime) m))+ pure i+ serverSyncProcessorChangeItem si st a =+ modify+ ( \(ServerStore m) ->+ let m' = M.adjust (const (Timed a st)) si m+ in ServerStore m'+ )+ serverSyncProcessorDeleteItem si =+ modify+ ( \(ServerStore m) ->+ let m' = M.delete si m+ in ServerStore m'+ )++distinct :: Eq a => [a] -> Bool+distinct ls = nub ls == ls
src/Data/Mergeful/Item.hs view
@@ -39,50 +39,63 @@ -- 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.Item- ( initialClientItem- , initialItemSyncRequest- , makeItemSyncRequest- , mergeItemSyncResponseRaw- , ItemMergeResult(..)- , mergeItemSyncResponseIgnoreProblems- , mergeIgnoringProblems- , mergeFromServer- , ItemMergeStrategy(..)- , mergeUsingStrategy- -- * Server side- , initialServerItem- , processServerItemSync- -- * Types, for reference- , ClientItem(..)- , ItemSyncRequest(..)- , ItemSyncResponse(..)- , ServerItem(..)- ) where+ ( initialClientItem,+ initialItemSyncRequest,+ makeItemSyncRequest,+ mergeItemSyncResponseFromServer,+ mergeItemSyncResponseFromClient,+ mergeItemSyncResponseUsingCRDT,+ mergeItemSyncResponseUsingStrategy,+ mergeFromServer,+ mergeFromServerStrategy,+ mergeFromClient,+ mergeFromClientStrategy,+ mergeUsingCRDT,+ mergeUsingCRDTStrategy,+ ItemMergeStrategy (..),+ ChangeConflictResolution (..),+ ClientDeletedConflictResolution (..),+ ServerDeletedConflictResolution (..),+ mergeUsingStrategy,+ mergeItemSyncResponseRaw,+ ItemMergeResult (..), -import GHC.Generics (Generic)+ -- * Server side+ initialServerItem,+ processServerItemSync, -import Data.Aeson as JSON-import Data.Validity+ -- * Types, for reference+ ClientItem (..),+ ItemSyncRequest (..),+ ItemSyncResponse (..),+ ServerItem (..),+ )+where import Control.Applicative-+import Control.DeepSeq+import Data.Aeson as JSON import Data.Mergeful.Timed+import Data.Validity+import GHC.Generics (Generic) data ClientItem a- -- | There is no item on the client side- = ClientEmpty- -- | There is is an item but the server is not aware of it yet.- | ClientAdded !a- -- | There is is an item and it has been synced with the server.- | ClientItemSynced !(Timed a)- -- | There is is an item and it has been synced with the server, but it has since been modified.- | ClientItemSyncedButChanged !(Timed a)- -- | There was an item, and it has been deleted locally, but the server has not been made aware of this.- | ClientDeleted !ServerTime+ = -- | There is no item on the client side+ ClientEmpty+ | -- | There is is an item but the server is not aware of it yet.+ ClientAdded !a+ | -- | There is is an item and it has been synced with the server.+ ClientItemSynced !(Timed a)+ | -- | There is is an item and it has been synced with the server, but it has since been modified.+ ClientItemSyncedButChanged !(Timed a)+ | -- | There was an item, and it has been deleted locally, but the server has not been made aware of this.+ ClientDeleted !ServerTime deriving (Show, Eq, Generic) instance Validity a => Validity (ClientItem a) +instance NFData a => NFData (ClientItem a)+ instance FromJSON a => FromJSON (ClientItem a) where parseJSON = withObject "ClientItem" $ \o -> do@@ -98,14 +111,14 @@ instance ToJSON a => ToJSON (ClientItem a) where toJSON ci = object $- case ci of- ClientEmpty -> ["type" .= ("empty" :: String)]- ClientAdded a -> ["type" .= ("added" :: String), "value" .= a]- ClientItemSynced Timed {..} ->- ["type" .= ("synced" :: String), "value" .= timedValue, "time" .= timedTime]- ClientItemSyncedButChanged Timed {..} ->- ["type" .= ("changed" :: String), "value" .= timedValue, "time" .= timedTime]- ClientDeleted t -> ["type" .= ("deleted" :: String), "time" .= t]+ case ci of+ ClientEmpty -> ["type" .= ("empty" :: String)]+ ClientAdded a -> ["type" .= ("added" :: String), "value" .= a]+ ClientItemSynced Timed {..} ->+ ["type" .= ("synced" :: String), "value" .= timedValue, "time" .= timedTime]+ ClientItemSyncedButChanged Timed {..} ->+ ["type" .= ("changed" :: String), "value" .= timedValue, "time" .= timedTime]+ ClientDeleted t -> ["type" .= ("deleted" :: String), "time" .= t] -- | A client item to start with. --@@ -114,14 +127,16 @@ initialClientItem = ClientEmpty data ServerItem a- -- | There is no item on the server side- = ServerEmpty- -- | There is an item on the server side, and it was last synced at the given 'ServerTime'.- | ServerFull !(Timed a)+ = -- | There is no item on the server side+ ServerEmpty+ | -- | There is an item on the server side, and it was last synced at the given 'ServerTime'.+ ServerFull !(Timed a) deriving (Show, Eq, Generic) instance Validity a => Validity (ServerItem a) +instance NFData a => NFData (ServerItem a)+ instance FromJSON a => FromJSON (ServerItem a) where parseJSON = withObject "ServerItem" $ \o ->@@ -130,9 +145,9 @@ instance ToJSON a => ToJSON (ServerItem a) where toJSON si = object $- case si of- ServerEmpty -> []- ServerFull Timed {..} -> ["value" .= timedValue, "time" .= timedTime]+ case si of+ ServerEmpty -> []+ ServerFull Timed {..} -> ["value" .= timedValue, "time" .= timedTime] -- | A server item to start with. --@@ -141,22 +156,24 @@ initialServerItem = ServerEmpty data ItemSyncRequest a- -- | There is no item locally- = ItemSyncRequestPoll- -- | There is an item locally that has not been synced to the server yet.- | ItemSyncRequestNew !a- -- | There is an item locally that was synced at the given 'ServerTime'- | ItemSyncRequestKnown !ServerTime- -- | There is an item locally that was synced at the given 'ServerTime'- -- but it has been changed since then.- | ItemSyncRequestKnownButChanged !(Timed a)- -- | There was an item locally that has been deleted but the- -- deletion wasn't synced to the server yet.- | ItemSyncRequestDeletedLocally !ServerTime+ = -- | There is no item locally+ ItemSyncRequestPoll+ | -- | There is an item locally that has not been synced to the server yet.+ ItemSyncRequestNew !a+ | -- | There is an item locally that was synced at the given 'ServerTime'+ ItemSyncRequestKnown !ServerTime+ | -- | There is an item locally that was synced at the given 'ServerTime'+ -- but it has been changed since then.+ ItemSyncRequestKnownButChanged !(Timed a)+ | -- | There was an item locally that has been deleted but the+ -- deletion wasn't synced to the server yet.+ ItemSyncRequestDeletedLocally !ServerTime deriving (Show, Eq, Generic) instance Validity a => Validity (ItemSyncRequest a) +instance NFData a => NFData (ItemSyncRequest a)+ instance FromJSON a => FromJSON (ItemSyncRequest a) where parseJSON = withObject "ItemSyncRequest" $ \o -> do@@ -172,15 +189,15 @@ instance ToJSON a => ToJSON (ItemSyncRequest a) where toJSON ci = object $- let o n rest = ("type" .= (n :: String)) : rest- oe n = o n []- in case ci of- ItemSyncRequestPoll -> oe "empty"- ItemSyncRequestNew a -> o "added" ["value" .= a]- ItemSyncRequestKnown t -> o "synced" ["time" .= t]- ItemSyncRequestKnownButChanged Timed {..} ->- o "changed" ["value" .= timedValue, "time" .= timedTime]- ItemSyncRequestDeletedLocally t -> o "deleted" ["time" .= t]+ let o n rest = ("type" .= (n :: String)) : rest+ oe n = o n []+ in case ci of+ ItemSyncRequestPoll -> oe "empty"+ ItemSyncRequestNew a -> o "added" ["value" .= a]+ ItemSyncRequestKnown t -> o "synced" ["time" .= t]+ ItemSyncRequestKnownButChanged Timed {..} ->+ o "changed" ["value" .= timedValue, "time" .= timedTime]+ ItemSyncRequestDeletedLocally t -> o "deleted" ["time" .= t] -- | An intial 'ItemSyncRequest' to start with. --@@ -189,60 +206,59 @@ initialItemSyncRequest = ItemSyncRequestPoll data ItemSyncResponse a- -- | The client and server are fully in sync, and both empty- --- -- Nothing needs to be done at the client side.- = ItemSyncResponseInSyncEmpty- -- | The client and server are fully in sync.- --- -- Nothing needs to be done at the client side.- | ItemSyncResponseInSyncFull- -- | The client added an item and server has succesfully been made aware of that.- --- -- The client needs to update its server time- | ItemSyncResponseClientAdded !ServerTime- -- | The client changed an item and server has succesfully been made aware of that.- --- -- The client needs to update its server time- | ItemSyncResponseClientChanged !ServerTime- -- | The client deleted an item and server has succesfully been made aware of that.- --- -- The client can delete it from its deleted items- | ItemSyncResponseClientDeleted- -- | This item has been added on the server side- --- -- The client should add it too.- | ItemSyncResponseServerAdded !(Timed a)- -- | This item has been modified on the server side.- --- -- The client should modify it too.- | ItemSyncResponseServerChanged !(Timed a)- -- | The item was deleted on the server side- --- -- The client should delete it too.- | ItemSyncResponseServerDeleted- -- | A conflict occurred.- --- -- The server and the client both have an item, but it is different.- -- The server kept its part, the client can either take whatever the server gave them- -- or deal with the conflict somehow, and then try to re-sync.- | ItemSyncResponseConflict !(Timed a) -- ^ The item at the server side- -- | A conflict occurred.- --- -- The server has an item but the client does not.- -- The server kept its part, the client can either take whatever the server gave them- -- or deal with the conflict somehow, and then try to re-sync.- | ItemSyncResponseConflictClientDeleted !(Timed a) -- ^ The item at the server side- -- | A conflict occurred.- --- -- The client has a (modified) item but the server does not have any item.- -- The server left its item deleted, the client can either delete its item too- -- or deal with the conflict somehow, and then try to re-sync.+ = -- | The client and server are fully in sync, and both empty+ --+ -- Nothing needs to be done at the client side.+ ItemSyncResponseInSyncEmpty+ | -- | The client and server are fully in sync.+ --+ -- Nothing needs to be done at the client side.+ ItemSyncResponseInSyncFull+ | -- | The client added an item and server has succesfully been made aware of that.+ --+ -- The client needs to update its server time+ ItemSyncResponseClientAdded !ServerTime+ | -- | The client changed an item and server has succesfully been made aware of that.+ --+ -- The client needs to update its server time+ ItemSyncResponseClientChanged !ServerTime+ | -- | The client deleted an item and server has succesfully been made aware of that.+ --+ -- The client can delete it from its deleted items+ ItemSyncResponseClientDeleted+ | -- | This item has been added on the server side+ --+ -- The client should add it too.+ ItemSyncResponseServerAdded !(Timed a)+ | -- | This item has been modified on the server side.+ --+ -- The client should modify it too.+ ItemSyncResponseServerChanged !(Timed a)+ | -- | The item was deleted on the server side+ --+ -- The client should delete it too.+ ItemSyncResponseServerDeleted+ | -- | The item at the server side+ -- | A conflict occurred.+ --+ -- The server has an item but the client does not.+ -- The server kept its part, the client can either take whatever the server gave them+ -- or deal with the conflict somehow, and then try to re-sync.+ ItemSyncResponseConflict !(Timed a)+ | -- | The item at the server side+ -- | A conflict occurred.+ --+ -- The client has a (modified) item but the server does not have any item.+ -- The server left its item deleted, the client can either delete its item too+ -- or deal with the conflict somehow, and then try to re-sync.+ ItemSyncResponseConflictClientDeleted !(Timed a) | ItemSyncResponseConflictServerDeleted deriving (Show, Eq, Generic) instance Validity a => Validity (ItemSyncResponse a) +instance NFData a => NFData (ItemSyncResponse a)+ instance FromJSON a => FromJSON (ItemSyncResponse a) where parseJSON = withObject "ItemSyncResponse" $ \o -> do@@ -265,22 +281,22 @@ instance ToJSON a => ToJSON (ItemSyncResponse a) where toJSON isr = object $- let o s rest = ("type" .= (s :: String)) : rest- oe s = o s []- in case isr of- ItemSyncResponseInSyncEmpty -> oe "in-sync-empty"- ItemSyncResponseInSyncFull -> oe "in-sync-full"- ItemSyncResponseClientAdded t -> o "client-added" ["time" .= t]- ItemSyncResponseClientChanged t -> o "client-changed" ["time" .= t]- ItemSyncResponseClientDeleted -> oe "client-deleted"- ItemSyncResponseServerAdded Timed {..} ->- o "server-added" ["value" .= timedValue, "time" .= timedTime]- ItemSyncResponseServerChanged Timed {..} ->- o "server-changed" ["value" .= timedValue, "time" .= timedTime]- ItemSyncResponseServerDeleted -> oe "server-deleted"- ItemSyncResponseConflict a -> o "conflict" ["value" .= a]- ItemSyncResponseConflictClientDeleted a -> o "conflict-client-deleted" ["value" .= a]- ItemSyncResponseConflictServerDeleted -> oe "conflict-server-deleted"+ let o s rest = ("type" .= (s :: String)) : rest+ oe s = o s []+ in case isr of+ ItemSyncResponseInSyncEmpty -> oe "in-sync-empty"+ ItemSyncResponseInSyncFull -> oe "in-sync-full"+ ItemSyncResponseClientAdded t -> o "client-added" ["time" .= t]+ ItemSyncResponseClientChanged t -> o "client-changed" ["time" .= t]+ ItemSyncResponseClientDeleted -> oe "client-deleted"+ ItemSyncResponseServerAdded Timed {..} ->+ o "server-added" ["value" .= timedValue, "time" .= timedTime]+ ItemSyncResponseServerChanged Timed {..} ->+ o "server-changed" ["value" .= timedValue, "time" .= timedTime]+ ItemSyncResponseServerDeleted -> oe "server-deleted"+ ItemSyncResponseConflict a -> o "conflict" ["value" .= a]+ ItemSyncResponseConflictClientDeleted a -> o "conflict-client-deleted" ["value" .= a]+ ItemSyncResponseConflictServerDeleted -> oe "conflict-server-deleted" -- | Produce an 'ItemSyncRequest' from a 'ClientItem'. --@@ -295,20 +311,24 @@ ClientDeleted st -> ItemSyncRequestDeletedLocally st data ItemMergeResult a- -- | The merger went succesfully, no conflicts or desyncs- = MergeSuccess !(ClientItem a)- -- | There was a merge conflict. The server and client had different, conflicting versions.- | MergeConflict !a !(Timed a) -- ^ The item at the server side- -- | There was a merge conflict. The client had deleted the item while the server had modified it.- | MergeConflictClientDeleted !(Timed a) -- ^ The item at the server side- -- | There was a merge conflict. The server had deleted the item while the client had modified it.- | MergeConflictServerDeleted !a -- ^ The item at the client side- -- | The server responded with a response that did not make sense given the client's request.+ = -- | The merger went succesfully, no conflicts or desyncs+ MergeSuccess !(ClientItem a)+ | -- | The item at the server side+ -- | There was a merge conflict. The client had deleted the item while the server had modified it.+ MergeConflict !a !(Timed a)+ | -- | The item at the server side+ -- | There was a merge conflict. The server had deleted the item while the client had modified it.+ MergeConflictClientDeleted !(Timed a)+ | -- | The item at the client side+ -- | The server responded with a response that did not make sense given the client's request.+ MergeConflictServerDeleted !a | MergeMismatch deriving (Show, Eq, Generic) instance Validity a => Validity (ItemMergeResult a) +instance NFData a => NFData (ItemMergeResult a)+ -- | Merge an 'ItemSyncResponse' into the current 'ClientItem'. -- -- This function will not make any decisions about what to do with@@ -346,48 +366,48 @@ ItemSyncResponseConflictClientDeleted si -> MergeConflictClientDeleted si _ -> MergeMismatch --- | Merge an 'ItemSyncResponse' into the current 'ClientItem'.------ 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.------ > mergeItemSyncResponseIgnoreProblems cs = mergeIgnoringProblems cs . mergeItemSyncResponseRaw cs-mergeItemSyncResponseIgnoreProblems :: ClientItem a -> ItemSyncResponse a -> ClientItem a-mergeItemSyncResponseIgnoreProblems cs = mergeIgnoringProblems cs . mergeItemSyncResponseRaw cs+mergeItemSyncResponseUsingStrategy :: ItemMergeStrategy a -> ClientItem a -> ItemSyncResponse a -> ClientItem a+mergeItemSyncResponseUsingStrategy strat ci sr = mergeUsingStrategy strat ci $ mergeItemSyncResponseRaw ci sr +mergeItemSyncResponseFromServer :: ClientItem a -> ItemSyncResponse a -> ClientItem a+mergeItemSyncResponseFromServer = mergeItemSyncResponseUsingStrategy mergeFromServerStrategy++mergeItemSyncResponseFromClient :: ClientItem a -> ItemSyncResponse a -> ClientItem a+mergeItemSyncResponseFromClient = mergeItemSyncResponseUsingStrategy mergeFromClientStrategy++mergeItemSyncResponseUsingCRDT :: (a -> a -> a) -> ClientItem a -> ItemSyncResponse a -> ClientItem a+mergeItemSyncResponseUsingCRDT = mergeItemSyncResponseUsingStrategy . mergeUsingCRDTStrategy+ -- | A strategy to merge conflicts for item synchronisation-data ItemMergeStrategy a =- ItemMergeStrategy- { itemMergeStrategyMergeChangeConflict :: a -> Timed a -> Timed a- -- ^ How to merge modification conflicts- , itemMergeStrategyMergeClientDeletedConflict :: Timed a -> Maybe (Timed a)- -- ^ How to merge conflicts where the client deleted an item that the server modified- , itemMergeStrategyMergeServerDeletedConflict :: a -> Maybe a- -- ^ How to merge conflicts where the server deleted an item that the client modified- }+data ItemMergeStrategy a+ = ItemMergeStrategy+ { -- | How to merge modification conflicts+ --+ -- The first argument is the client item and the second argument is the server item.+ itemMergeStrategyMergeChangeConflict :: a -> a -> ChangeConflictResolution a,+ -- | How to merge conflicts where the client deleted an item that the server modified+ itemMergeStrategyMergeClientDeletedConflict :: a -> ClientDeletedConflictResolution,+ -- | How to merge conflicts where the server deleted an item that the client modified+ itemMergeStrategyMergeServerDeletedConflict :: a -> ServerDeletedConflictResolution+ } deriving (Generic) --- | Ignore any merge problems in a 'ItemMergeResult'.------ This function just returns the original 'ClientItem' if anything other than 'MergeSuccess' occurs.------ 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: does not lose data------ __Con: Clients will diverge when a conflict occurs__-mergeIgnoringProblems :: ClientItem a -> ItemMergeResult a -> ClientItem a-mergeIgnoringProblems cs mr =- case mr of- MergeSuccess cs' -> cs'- MergeConflict _ _ -> cs- MergeConflictServerDeleted _ -> cs- MergeConflictClientDeleted _ -> cs- MergeMismatch -> cs+data ChangeConflictResolution a+ = KeepLocal+ | TakeRemote+ | Merged a+ deriving (Show, Eq, Generic) +data ClientDeletedConflictResolution+ = TakeRemoteChange+ | StayDeleted+ deriving (Show, Eq, Generic)++data ServerDeletedConflictResolution+ = KeepLocalChange+ | Delete+ deriving (Show, Eq, Generic)+ -- | Resolve an 'ItemMergeResult' using a given merge strategy. -- -- This function ignores 'MergeMismatch' and will just return the original 'ClientItem' in that case.@@ -398,20 +418,30 @@ -- * Idempotent -- * The same on all clients mergeUsingStrategy :: ItemMergeStrategy a -> ClientItem a -> ItemMergeResult a -> ClientItem a-mergeUsingStrategy ItemMergeStrategy {..} cs mr =+mergeUsingStrategy ItemMergeStrategy {..} ci mr = case mr of- MergeSuccess cs' -> cs'- MergeConflict a1 a2 -> ClientItemSynced $ itemMergeStrategyMergeChangeConflict a1 a2- MergeConflictClientDeleted sa ->- case itemMergeStrategyMergeClientDeletedConflict sa of- Nothing -> ClientEmpty- Just t -> ClientItemSynced t- MergeConflictServerDeleted ca ->- case itemMergeStrategyMergeServerDeletedConflict ca of- Nothing -> ClientEmpty- Just a -> ClientAdded a- MergeMismatch -> cs+ MergeSuccess ci' -> ci'+ MergeConflict a1 ri -> mergeChangeConflict itemMergeStrategyMergeChangeConflict ci a1 ri+ MergeConflictClientDeleted ri -> mergeClientDeletedConflict itemMergeStrategyMergeClientDeletedConflict ci ri+ MergeConflictServerDeleted ca -> mergeServerDeletedConflict itemMergeStrategyMergeServerDeletedConflict ci ca+ MergeMismatch -> ci +mergeChangeConflict :: (a -> a -> ChangeConflictResolution a) -> ClientItem a -> a -> Timed a -> ClientItem a+mergeChangeConflict func ci a1 ri@(Timed a2 st) = case func a1 a2 of+ KeepLocal -> ci+ TakeRemote -> ClientItemSynced ri+ Merged ma -> ClientItemSynced $ Timed ma st++mergeClientDeletedConflict :: (a -> ClientDeletedConflictResolution) -> ClientItem a -> Timed a -> ClientItem a+mergeClientDeletedConflict func ci ri@(Timed sa _) = case func sa of+ TakeRemoteChange -> ClientItemSynced ri+ StayDeleted -> ci -- We can't just use 'ClientEmpty' here because otherwise the 'mergeUsingStrategy' wouldn't be idempotent anymore.++mergeServerDeletedConflict :: (a -> ServerDeletedConflictResolution) -> ClientItem a -> a -> ClientItem a+mergeServerDeletedConflict func ci ca = case func ca of+ KeepLocalChange -> ci -- We can't just use 'ClientAdded ca' here because otherwise the 'mergeUsingStrategy' wouldn't be idempotent anymore.+ Delete -> ClientEmpty+ -- | Resolve an 'ItemMergeResult' by taking whatever the server gave the client. -- -- Pro: Clients will converge on the same value.@@ -419,13 +449,55 @@ -- __Con: Conflicting updates will be lost.__ mergeFromServer :: ClientItem a -> ItemMergeResult a -> ClientItem a mergeFromServer =- mergeUsingStrategy- ItemMergeStrategy- { itemMergeStrategyMergeChangeConflict = \_ serverItem -> serverItem- , itemMergeStrategyMergeClientDeletedConflict = \serverItem -> Just serverItem- , itemMergeStrategyMergeServerDeletedConflict = \_ -> Nothing- }+ mergeUsingStrategy mergeFromServerStrategy +-- | A merge strategy that takes whatever the server gave the client.+--+-- Pro: Clients will converge on the same value.+--+-- __Con: Conflicting updates will be lost.__+mergeFromServerStrategy :: ItemMergeStrategy a+mergeFromServerStrategy =+ ItemMergeStrategy+ { itemMergeStrategyMergeChangeConflict = \_ _ -> TakeRemote,+ itemMergeStrategyMergeClientDeletedConflict = \_ -> TakeRemoteChange,+ itemMergeStrategyMergeServerDeletedConflict = \_ -> Delete+ }++-- | Resolve an 'ItemMergeResult' by keeping whatever the client had.+--+-- Pro: does not lose data+--+-- __Con: Clients will diverge when a conflict occurs__+mergeFromClient :: ClientItem a -> ItemMergeResult a -> ClientItem a+mergeFromClient = mergeUsingStrategy mergeFromClientStrategy++-- | A merge strategy that keeps whatever the client had.+--+-- Pro: does not lose data+--+-- __Con: Clients will diverge when a conflict occurs__+mergeFromClientStrategy :: ItemMergeStrategy a+mergeFromClientStrategy =+ ItemMergeStrategy+ { itemMergeStrategyMergeChangeConflict = \_ _ -> KeepLocal,+ itemMergeStrategyMergeClientDeletedConflict = \_ -> StayDeleted,+ itemMergeStrategyMergeServerDeletedConflict = \_ -> KeepLocalChange+ }++mergeUsingCRDT :: (a -> a -> a) -> ClientItem a -> ItemMergeResult a -> ClientItem a+mergeUsingCRDT = mergeUsingStrategy . mergeUsingCRDTStrategy++-- | A merge strategy that uses a CRDT merging function to merge items.+--+-- In case of other-than-change conflicts, this will be the same as the 'mergeFromServerStrategy' strategy.+-- If this is not what you want, create your own 'ItemMergeStrategy' manually.+mergeUsingCRDTStrategy :: (a -> a -> a) -> ItemMergeStrategy a+mergeUsingCRDTStrategy merge =+ mergeFromServerStrategy+ { itemMergeStrategyMergeChangeConflict = \a1 a2 -> Merged (merge a1 a2)+ }+ -- | Serve an 'ItemSyncRequest' using the current 'ServerItem', producing an 'ItemSyncResponse' and a new 'ServerItem'. processServerItemSync :: ServerItem a -> ItemSyncRequest a -> (ItemSyncResponse a, ServerItem a) processServerItemSync store sr =@@ -436,79 +508,83 @@ ItemSyncRequestPoll -> (ItemSyncResponseInSyncEmpty, store) ItemSyncRequestNew ci -> (ItemSyncResponseClientAdded t, ServerFull $ Timed {timedValue = ci, timedTime = t})- ItemSyncRequestKnown _- -- This indicates that the server synced with another client and was told to- -- delete its item.- --- -- Given that the client indicates that it did not change anything locally,- -- the server will just instruct the client to delete its item too.- -- No conflict here.- -> (ItemSyncResponseServerDeleted, store)- ItemSyncRequestKnownButChanged _- -- This indicates that the server synced with another client and was told to- -- delete its item.- --- -- Given that the client indicates that it *did* change its item locally,- -- there is a conflict.- -> (ItemSyncResponseConflictServerDeleted, store)- ItemSyncRequestDeletedLocally _- -- This means that the server synced with another client,- -- was instructed to delete its item by that client,- -- and is now being told to delete its item again.- --- -- That's fine, it will just remain deleted.- -- No conflict here- -> (ItemSyncResponseClientDeleted, store)+ ItemSyncRequestKnown _ ->+ -- This indicates that the server synced with another client and was told to+ -- delete its item.+ --+ -- Given that the client indicates that it did not change anything locally,+ -- the server will just instruct the client to delete its item too.+ -- No conflict here.+ (ItemSyncResponseServerDeleted, store)+ ItemSyncRequestKnownButChanged _ ->+ -- This indicates that the server synced with another client and was told to+ -- delete its item.+ --+ -- Given that the client indicates that it *did* change its item locally,+ -- there is a conflict.+ (ItemSyncResponseConflictServerDeleted, store)+ ItemSyncRequestDeletedLocally _ ->+ -- This means that the server synced with another client,+ -- was instructed to delete its item by that client,+ -- and is now being told to delete its item again.+ --+ -- That's fine, it will just remain deleted.+ -- No conflict here+ (ItemSyncResponseClientDeleted, store) ServerFull t@(Timed si st) -> let st' = incrementServerTime st in case sr of- ItemSyncRequestPoll+ ItemSyncRequestPoll -> -- The client is empty but the server is not. -- This means that the server has synced with another client before, -- so we can just send the item to the client.- -> (ItemSyncResponseServerAdded (Timed {timedValue = si, timedTime = st}), store)- ItemSyncRequestNew _+ (ItemSyncResponseServerAdded (Timed {timedValue = si, timedTime = st}), store)+ ItemSyncRequestNew _ -> -- The client has a newly added item, so it thought it was empty before that, -- but the server has already synced with another client before. -- This indicates a conflict. -- The server is always right, so the item at the server will remain unmodified. -- The client will receive the conflict.- -> (ItemSyncResponseConflict t, store)+ (ItemSyncResponseConflict t, store) ItemSyncRequestKnown ct -> if ct >= st- -- The client time is equal to the server time.+ then-- The client time is equal to the server time. -- The client indicates that the item was not modified at their side. -- This means that the items are in sync. -- (Unless the server somehow modified the item but not its server time, -- which would beconsidered a bug.)- then (ItemSyncResponseInSyncFull, store)- -- The client time is less than the server time+ (ItemSyncResponseInSyncFull, store)+ else-- The client time is less than the server time -- That means that the server has synced with another client in the meantime. -- Since the client indicates that the item was not modified at their side, -- we can just send it back to the client to have them update their version. -- No conflict here.- else ( ItemSyncResponseServerChanged (Timed {timedValue = si, timedTime = st})- , store)++ ( ItemSyncResponseServerChanged (Timed {timedValue = si, timedTime = st}),+ store+ ) ItemSyncRequestKnownButChanged Timed {timedValue = ci, timedTime = ct} -> if ct >= st- -- The client time is equal to the server time.+ then-- The client time is equal to the server time. -- The client indicates that the item *was* modified at their side. -- This means that the server needs to be updated.- then ( ItemSyncResponseClientChanged st'- , ServerFull (Timed {timedValue = ci, timedTime = st'}))- -- The client time is less than the server time++ ( ItemSyncResponseClientChanged st',+ ServerFull (Timed {timedValue = ci, timedTime = st'})+ )+ else-- The client time is less than the server time -- That means that the server has synced with another client in the meantime. -- Since the client indicates that the item *was* modified at their side, -- there is a conflict.- else (ItemSyncResponseConflict t, store)+ (ItemSyncResponseConflict t, store) ItemSyncRequestDeletedLocally ct -> if ct >= st- -- The client time is equal to the server time.+ then-- The client time is equal to the server time. -- The client indicates that the item was deleted on their side. -- This means that the server item needs to be deleted as well.- then (ItemSyncResponseClientDeleted, ServerEmpty)- -- The client time is less than the server time+ (ItemSyncResponseClientDeleted, ServerEmpty)+ else-- The client time is less than the server time -- That means that the server has synced with another client in the meantime. -- Since the client indicates that the item was deleted at their side, -- there is a conflict.- else (ItemSyncResponseConflictClientDeleted t, store)+ (ItemSyncResponseConflictClientDeleted t, store)
src/Data/Mergeful/Timed.hs view
@@ -1,23 +1,24 @@ {-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-} -- | Dealing with server times. -- -- __If you are importing this module, you are probably doing something wrong.__ module Data.Mergeful.Timed- ( ServerTime- , initialServerTime- , incrementServerTime- , Timed(..)- ) where--import GHC.Generics (Generic)+ ( ServerTime (..),+ initialServerTime,+ incrementServerTime,+ Timed (..),+ )+where +import Control.DeepSeq import Data.Aeson as JSON import Data.Validity import Data.Word+import GHC.Generics (Generic) -- | A "time", as "measured" by the server. --@@ -25,31 +26,41 @@ -- distinction should not matter for your usage of this library. -- -- In any case, a client should not be changing this value.-newtype ServerTime =- ServerTime- { unServerTime :: Word64- }+--+-- We use a 'Word64' instead of a natural.+-- This will go wrong after 2^64 versions, but since that+-- will not happen in practice, we will not worry about it.+-- You would have to sync millions of modifications every second+-- until long after the sun consumes the earth for this to be a problem.+newtype ServerTime+ = ServerTime+ { unServerTime :: Word64+ } deriving (Show, Eq, Ord, Generic, ToJSON, FromJSON) instance Validity ServerTime +instance NFData ServerTime+ -- | A server time to start with. initialServerTime :: ServerTime initialServerTime = ServerTime 0 --- | Increment a server time, this will only start repeating after at least @2^64 - 1@ values, so you're probably fine.+-- | Increment a server time. incrementServerTime :: ServerTime -> ServerTime incrementServerTime (ServerTime w) = ServerTime (succ w) -- | A value along with a server time.-data Timed a =- Timed- { timedValue :: !a- , timedTime :: !ServerTime- }+data Timed a+ = Timed+ { timedValue :: !a,+ timedTime :: !ServerTime+ } deriving (Show, Eq, Generic) instance Validity a => Validity (Timed a)++instance NFData a => NFData (Timed a) instance FromJSON a => FromJSON (Timed a) where parseJSON = withObject "Timed" $ \o -> Timed <$> o .: "value" <*> o .: "time"
src/Data/Mergeful/Value.hs view
@@ -38,31 +38,33 @@ -- 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.Value- ( initialClientValue- , makeValueSyncRequest- , mergeValueSyncResponseRaw- , ValueMergeResult(..)- , mergeValueSyncResponseIgnoreProblems- , mergeIgnoringProblems- , mergeFromServer- , mergeUsingFunction+ ( initialClientValue,+ makeValueSyncRequest,+ mergeValueSyncResponseRaw,+ ValueMergeResult (..),+ mergeValueSyncResponseIgnoreProblems,+ mergeIgnoringProblems,+ mergeFromServer,+ mergeUsingFunction,+ -- * Server side- , initialServerValue- , processServerValueSync- -- * Types, for reference- , ChangedFlag(..)- , ClientValue(..)- , ValueSyncRequest(..)- , ValueSyncResponse(..)- , ServerValue(..)- ) where+ initialServerValue,+ processServerValueSync, -import GHC.Generics (Generic)+ -- * Types, for reference+ ChangedFlag (..),+ ClientValue (..),+ ValueSyncRequest (..),+ ValueSyncResponse (..),+ ServerValue (..),+ )+where +import Control.DeepSeq import Data.Aeson as JSON-import Data.Validity- import Data.Mergeful.Timed+import Data.Validity+import GHC.Generics (Generic) data ChangedFlag = Changed@@ -71,6 +73,8 @@ instance Validity ChangedFlag +instance NFData ChangedFlag+ -- | The client side value. -- -- The only differences between `a` and 'ClientValue a' are that@@ -78,31 +82,36 @@ -- the server, and whether the item has been modified at the client -- -- There cannot be an unsynced 'ClientValue'.-data ClientValue a =- ClientValue !(Timed a) !ChangedFlag+data ClientValue a+ = ClientValue !(Timed a) !ChangedFlag deriving (Show, Eq, Generic) instance Validity a => Validity (ClientValue a) +instance NFData a => NFData (ClientValue a)+ instance FromJSON a => FromJSON (ClientValue a) where parseJSON = withObject "ClientValue" $ \o ->- ClientValue <$> (Timed <$> o .: "value" <*> o .: "time") <*>- ((\b ->- if b- then Changed- else NotChanged) <$>- o .: "changed")+ ClientValue <$> (Timed <$> o .: "value" <*> o .: "time")+ <*> ( ( \b ->+ if b+ then Changed+ else NotChanged+ )+ <$> o .: "changed"+ ) instance ToJSON a => ToJSON (ClientValue a) where toJSON (ClientValue Timed {..} cf) = object- [ "value" .= timedValue- , "time" .= timedTime- , "changed" .=- (case cf of- Changed -> True- NotChanged -> False)+ [ "value" .= timedValue,+ "time" .= timedTime,+ "changed"+ .= ( case cf of+ Changed -> True+ NotChanged -> False+ ) ] -- | Produce a client value based on an initial synchronisation request@@ -113,12 +122,14 @@ -- -- The only difference between 'a' and 'ServerValue a' is that 'ServerValue a' also -- remembers the last time this value was changed during synchronisation.-newtype ServerValue a =- ServerValue (Timed a)+newtype ServerValue a+ = ServerValue (Timed a) deriving (Show, Eq, Generic) instance Validity a => Validity (ServerValue a) +instance NFData a => NFData (ServerValue a)+ instance FromJSON a => FromJSON (ServerValue a) where parseJSON = withObject "ServerValue" $ \o -> ServerValue <$> (Timed <$> o .: "value" <*> o .: "time")@@ -133,15 +144,17 @@ initialServerValue a = ServerValue $ Timed {timedValue = a, timedTime = initialServerTime} data ValueSyncRequest a- -- | There is an item locally that was synced at the given 'ServerTime'- = ValueSyncRequestKnown !ServerTime- -- | There is an item locally that was synced at the given 'ServerTime'- -- but it has been changed since then.- | ValueSyncRequestKnownButChanged !(Timed a)+ = -- | There is an item locally that was synced at the given 'ServerTime'+ ValueSyncRequestKnown !ServerTime+ | -- | There is an item locally that was synced at the given 'ServerTime'+ -- but it has been changed since then.+ ValueSyncRequestKnownButChanged !(Timed a) deriving (Show, Eq, Generic) instance Validity a => Validity (ValueSyncRequest a) +instance NFData a => NFData (ValueSyncRequest a)+ instance FromJSON a => FromJSON (ValueSyncRequest a) where parseJSON = withObject "ValueSyncRequest" $ \o -> do@@ -154,35 +167,33 @@ instance ToJSON a => ToJSON (ValueSyncRequest a) where toJSON ci = object $- let o n rest = ("type" .= (n :: String)) : rest- in case ci of- ValueSyncRequestKnown t -> o "synced" ["time" .= t]- ValueSyncRequestKnownButChanged Timed {..} ->- o "changed" ["value" .= timedValue, "time" .= timedTime]+ let o n rest = ("type" .= (n :: String)) : rest+ in case ci of+ ValueSyncRequestKnown t -> o "synced" ["time" .= t]+ ValueSyncRequestKnownButChanged Timed {..} ->+ o "changed" ["value" .= timedValue, "time" .= timedTime] data ValueSyncResponse a- -- | The client and server are fully in sync.- --- -- Nothing needs to be done at the client side.- = ValueSyncResponseInSync- -- | The client changed the value and server has succesfully been made aware of that.- --- -- The client needs to update its server time- | ValueSyncResponseClientChanged !ServerTime- -- | This value has been changed on the server side.- --- -- The client should change it too.- | ValueSyncResponseServerChanged !(Timed a)- -- | A conflict occurred.- --- -- The server and the client both changed the value in a conflicting manner.- -- The server kept its part, the client can either take whatever the server gave them- -- or deal with the conflict somehow, and then try to re-sync.- | ValueSyncResponseConflict !(Timed a) -- ^ The item at the server side+ = -- | The client and server are fully in sync.+ --+ -- Nothing needs to be done at the client side.+ ValueSyncResponseInSync+ | -- | The client changed the value and server has succesfully been made aware of that.+ --+ -- The client needs to update its server time+ ValueSyncResponseClientChanged !ServerTime+ | -- | This value has been changed on the server side.+ --+ -- The client should change it too.+ ValueSyncResponseServerChanged !(Timed a)+ | -- | The item at the server side+ ValueSyncResponseConflict !(Timed a) deriving (Show, Eq, Generic) instance Validity a => Validity (ValueSyncResponse a) +instance NFData a => NFData (ValueSyncResponse a)+ instance FromJSON a => FromJSON (ValueSyncResponse a) where parseJSON = withObject "ValueSyncResponse" $ \o -> do@@ -198,14 +209,14 @@ instance ToJSON a => ToJSON (ValueSyncResponse a) where toJSON isr = object $- let o s rest = ("type" .= (s :: String)) : rest- oe s = o s []- in case isr of- ValueSyncResponseInSync -> oe "in-sync"- ValueSyncResponseClientChanged t -> o "client-changed" ["time" .= t]- ValueSyncResponseServerChanged Timed {..} ->- o "server-changed" ["value" .= timedValue, "time" .= timedTime]- ValueSyncResponseConflict a -> o "conflict" ["value" .= a]+ let o s rest = ("type" .= (s :: String)) : rest+ oe s = o s []+ in case isr of+ ValueSyncResponseInSync -> oe "in-sync"+ ValueSyncResponseClientChanged t -> o "client-changed" ["time" .= t]+ ValueSyncResponseServerChanged Timed {..} ->+ o "server-changed" ["value" .= timedValue, "time" .= timedTime]+ ValueSyncResponseConflict a -> o "conflict" ["value" .= a] -- | Produce an 'ItemSyncRequest' from a 'ClientItem'. --@@ -217,18 +228,20 @@ Changed -> ValueSyncRequestKnownButChanged t data ValueMergeResult a- -- | The merger went succesfully, no conflicts or desyncs- = MergeSuccess !(ClientValue a)- -- | There was a merge conflict. The server and client had different, conflicting versions.- | MergeConflict !a !(Timed a) -- ^ The item at the server side- | MergeMismatch- -- ^ The server responded with a response that did not make sense given the client's request.- --- -- This should not happen in practice.+ = -- | The merger went succesfully, no conflicts or desyncs+ MergeSuccess !(ClientValue a)+ | -- | The item at the server side+ MergeConflict !a !(Timed a)+ | -- | The server responded with a response that did not make sense given the client's request.+ --+ -- This should not happen in practice.+ MergeMismatch deriving (Show, Eq, Generic) instance Validity a => Validity (ValueMergeResult a) +instance NFData a => NFData (ValueMergeResult a)+ -- | Merge an 'ValueSyncResponse' into the current 'ClientValue'. -- -- This function will not make any decisions about what to do with@@ -287,7 +300,7 @@ -- * Idempotent -- * The same on all clients mergeUsingFunction ::- (a -> Timed a -> Timed a) -> ClientValue a -> ValueMergeResult a -> ClientValue a+ (a -> Timed a -> Timed a) -> ClientValue a -> ValueMergeResult a -> ClientValue a mergeUsingFunction func cs mr = case mr of MergeSuccess cs' -> cs'@@ -304,33 +317,35 @@ -- | Serve an 'ValueSyncRequest' using the current 'ServerValue', producing an 'ValueSyncResponse' and a new 'ServerValue'. processServerValueSync ::- ServerValue a -> ValueSyncRequest a -> (ValueSyncResponse a, ServerValue a)+ ServerValue a -> ValueSyncRequest a -> (ValueSyncResponse a, ServerValue a) processServerValueSync sv@(ServerValue t@(Timed _ st)) sr = case sr of ValueSyncRequestKnown ct -> if ct >= st- -- The client time is equal to the server time.- -- The client indicates that the item was not modified at their side.- -- This means that the items are in sync.- -- (Unless the server somehow modified the item but not its server time,- -- which would beconsidered a bug.)- then (ValueSyncResponseInSync, sv)- -- The client time is less than the server time- -- That means that the server has synced with another client in the meantime.- -- Since the client indicates that the item was not modified at their side,- -- we can just send it back to the client to have them update their version.- -- No conflict here.- else (ValueSyncResponseServerChanged t, sv)+ then-- The client time is equal to the server time.+ -- The client indicates that the item was not modified at their side.+ -- This means that the items are in sync.+ -- (Unless the server somehow modified the item but not its server time,+ -- which would beconsidered a bug.)+ (ValueSyncResponseInSync, sv)+ else-- The client time is less than the server time+ -- That means that the server has synced with another client in the meantime.+ -- Since the client indicates that the item was not modified at their side,+ -- we can just send it back to the client to have them update their version.+ -- No conflict here.+ (ValueSyncResponseServerChanged t, sv) 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.- -- This means that the server needs to be updated.- then let st' = incrementServerTime st- in ( ValueSyncResponseClientChanged st'- , ServerValue (Timed {timedValue = ci, timedTime = st'}))- -- The client time is less than the server time- -- That means that the server has synced with another client in the meantime.- -- Since the client indicates that the item *was* modified at their side,- -- there is a conflict.- else (ValueSyncResponseConflict t, sv)+ then-- The client time is equal to the server time.+ -- The client indicates that the item *was* modified at their side.+ -- This means that the server needs to be updated.++ let st' = incrementServerTime st+ in ( ValueSyncResponseClientChanged st',+ ServerValue (Timed {timedValue = ci, timedTime = st'})+ )+ else-- The client time is less than the server time+ -- That means that the server has synced with another client in the meantime.+ -- Since the client indicates that the item *was* modified at their side,+ -- there is a conflict.+ (ValueSyncResponseConflict t, sv)