packages feed

mergeful 0.2.0.0 → 0.3.0.0

raw patch · 9 files changed

+632/−474 lines, 9 filesdep +autodocodecsetup-changedPVP ok

version bump matches the API change (PVP)

Dependencies added: autodocodec

API changes (from Hackage documentation)

- Data.Mergeful.Collection: [clientSyncProcessorSyncMergedConflict] :: ClientSyncProcessor ci si a -> !Map si (Timed a) -> m ()
- 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 (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 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.Item: instance Data.Aeson.Types.FromJSON.FromJSON a => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeful.Item.ClientItem a)
- Data.Mergeful.Item: instance Data.Aeson.Types.FromJSON.FromJSON a => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeful.Item.ItemSyncRequest a)
- Data.Mergeful.Item: instance Data.Aeson.Types.FromJSON.FromJSON a => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeful.Item.ItemSyncResponse a)
- Data.Mergeful.Item: instance Data.Aeson.Types.FromJSON.FromJSON a => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeful.Item.ServerItem a)
- Data.Mergeful.Item: instance Data.Aeson.Types.ToJSON.ToJSON a => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeful.Item.ClientItem a)
- Data.Mergeful.Item: instance Data.Aeson.Types.ToJSON.ToJSON a => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeful.Item.ItemSyncRequest a)
- Data.Mergeful.Item: instance Data.Aeson.Types.ToJSON.ToJSON a => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeful.Item.ItemSyncResponse a)
- Data.Mergeful.Item: instance Data.Aeson.Types.ToJSON.ToJSON a => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeful.Item.ServerItem a)
- Data.Mergeful.Timed: instance Data.Aeson.Types.FromJSON.FromJSON a => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeful.Timed.Timed a)
- Data.Mergeful.Timed: instance Data.Aeson.Types.ToJSON.ToJSON a => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeful.Timed.Timed a)
- Data.Mergeful.Value: instance Data.Aeson.Types.FromJSON.FromJSON a => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeful.Value.ClientValue a)
- Data.Mergeful.Value: instance Data.Aeson.Types.FromJSON.FromJSON a => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeful.Value.ServerValue a)
- Data.Mergeful.Value: instance Data.Aeson.Types.FromJSON.FromJSON a => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeful.Value.ValueSyncRequest a)
- Data.Mergeful.Value: instance Data.Aeson.Types.FromJSON.FromJSON a => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeful.Value.ValueSyncResponse a)
- Data.Mergeful.Value: instance Data.Aeson.Types.ToJSON.ToJSON a => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeful.Value.ClientValue a)
- Data.Mergeful.Value: instance Data.Aeson.Types.ToJSON.ToJSON a => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeful.Value.ServerValue a)
- Data.Mergeful.Value: instance Data.Aeson.Types.ToJSON.ToJSON a => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeful.Value.ValueSyncRequest a)
- Data.Mergeful.Value: instance Data.Aeson.Types.ToJSON.ToJSON a => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeful.Value.ValueSyncResponse a)
+ Data.Mergeful.Collection: [clientSyncProcessorSyncChangeConflictKeepLocal] :: ClientSyncProcessor ci si a (m :: Type -> Type) -> !Map si (Timed a) -> m ()
+ Data.Mergeful.Collection: [clientSyncProcessorSyncChangeConflictMerged] :: ClientSyncProcessor ci si a (m :: Type -> Type) -> !Map si (Timed a) -> m ()
+ Data.Mergeful.Collection: [clientSyncProcessorSyncChangeConflictTakeRemote] :: ClientSyncProcessor ci si a (m :: Type -> Type) -> !Map si (Timed a) -> m ()
+ Data.Mergeful.Collection: [clientSyncProcessorSyncClientDeletedConflictStayDeleted] :: ClientSyncProcessor ci si a (m :: Type -> Type) -> !Map si (Timed a) -> m ()
+ Data.Mergeful.Collection: [clientSyncProcessorSyncClientDeletedConflictTakeRemoteChanged] :: ClientSyncProcessor ci si a (m :: Type -> Type) -> !Map si (Timed a) -> m ()
+ Data.Mergeful.Collection: [clientSyncProcessorSyncServerDeletedConflictDelete] :: ClientSyncProcessor ci si a (m :: Type -> Type) -> !Set si -> m ()
+ Data.Mergeful.Collection: [clientSyncProcessorSyncServerDeletedConflictKeepLocalChange] :: ClientSyncProcessor ci si a (m :: Type -> Type) -> !Set si -> m ()
+ Data.Mergeful.Collection: instance (GHC.Classes.Ord ci, Data.Aeson.Types.FromJSON.FromJSONKey ci, Data.Aeson.Types.ToJSON.ToJSONKey ci, Autodocodec.Class.HasCodec ci, GHC.Classes.Ord si, Data.Aeson.Types.FromJSON.FromJSONKey si, Data.Aeson.Types.ToJSON.ToJSONKey si, Autodocodec.Class.HasCodec si, GHC.Classes.Eq a, Autodocodec.Class.HasCodec a) => Autodocodec.Class.HasCodec (Data.Mergeful.Collection.SyncResponse ci si a)
+ Data.Mergeful.Collection: instance (GHC.Classes.Ord ci, Data.Aeson.Types.FromJSON.FromJSONKey ci, Data.Aeson.Types.ToJSON.ToJSONKey ci, GHC.Classes.Ord si, Data.Aeson.Types.FromJSON.FromJSONKey si, Data.Aeson.Types.ToJSON.ToJSONKey si, GHC.Classes.Eq a, Autodocodec.Class.HasCodec a) => Autodocodec.Class.HasCodec (Data.Mergeful.Collection.ClientStore ci si a)
+ Data.Mergeful.Collection: instance (GHC.Classes.Ord ci, Data.Aeson.Types.FromJSON.FromJSONKey ci, Data.Aeson.Types.ToJSON.ToJSONKey ci, GHC.Classes.Ord si, Data.Aeson.Types.FromJSON.FromJSONKey si, Data.Aeson.Types.ToJSON.ToJSONKey si, GHC.Classes.Eq a, Autodocodec.Class.HasCodec a) => Autodocodec.Class.HasCodec (Data.Mergeful.Collection.SyncRequest 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.ToJSON.ToJSONKey ci, Data.Aeson.Types.ToJSON.ToJSONKey si, Autodocodec.Class.HasCodec a, GHC.Classes.Eq 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.ToJSON.ToJSONKey ci, Data.Aeson.Types.ToJSON.ToJSONKey si, Autodocodec.Class.HasCodec a, GHC.Classes.Eq a) => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeful.Collection.SyncRequest 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.ToJSON.ToJSONKey ci, Data.Aeson.Types.ToJSON.ToJSONKey si, Autodocodec.Class.HasCodec a, GHC.Classes.Eq a) => Data.Aeson.Types.ToJSON.ToJSON (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.ToJSON.ToJSONKey ci, Data.Aeson.Types.ToJSON.ToJSONKey si, Autodocodec.Class.HasCodec a, GHC.Classes.Eq a) => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeful.Collection.SyncRequest 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.ToJSON.ToJSONKey ci, Data.Aeson.Types.ToJSON.ToJSONKey si, Autodocodec.Class.HasCodec ci, Autodocodec.Class.HasCodec si, Autodocodec.Class.HasCodec a, GHC.Classes.Eq 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.ToJSON.ToJSONKey ci, Data.Aeson.Types.ToJSON.ToJSONKey si, Autodocodec.Class.HasCodec ci, Autodocodec.Class.HasCodec si, Autodocodec.Class.HasCodec a, GHC.Classes.Eq a) => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeful.Collection.SyncResponse ci si a)
+ Data.Mergeful.Collection: instance (GHC.Classes.Ord si, Data.Aeson.Types.FromJSON.FromJSONKey si, Data.Aeson.Types.ToJSON.ToJSONKey si, Autodocodec.Class.HasCodec a) => Autodocodec.Class.HasCodec (Data.Mergeful.Collection.ServerStore si a)
+ Data.Mergeful.Collection: instance (GHC.Classes.Ord si, Data.Aeson.Types.FromJSON.FromJSONKey si, Data.Aeson.Types.ToJSON.ToJSONKey si, Autodocodec.Class.HasCodec a) => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeful.Collection.ServerStore si a)
+ Data.Mergeful.Collection: instance (GHC.Classes.Ord si, Data.Aeson.Types.FromJSON.FromJSONKey si, Data.Aeson.Types.ToJSON.ToJSONKey si, Autodocodec.Class.HasCodec a) => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeful.Collection.ServerStore si a)
+ Data.Mergeful.Collection: instance Autodocodec.Class.HasCodec Data.Mergeful.Collection.ClientId
+ Data.Mergeful.Collection: instance Autodocodec.Class.HasCodec i => Autodocodec.Class.HasCodec (Data.Mergeful.Collection.ClientAddition i)
+ Data.Mergeful.Collection: instance Autodocodec.Class.HasCodec i => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeful.Collection.ClientAddition i)
+ Data.Mergeful.Collection: instance Autodocodec.Class.HasCodec i => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeful.Collection.ClientAddition i)
+ Data.Mergeful.Item: instance Autodocodec.Class.HasCodec a => Autodocodec.Class.HasCodec (Data.Mergeful.Item.ClientItem a)
+ Data.Mergeful.Item: instance Autodocodec.Class.HasCodec a => Autodocodec.Class.HasCodec (Data.Mergeful.Item.ItemSyncRequest a)
+ Data.Mergeful.Item: instance Autodocodec.Class.HasCodec a => Autodocodec.Class.HasCodec (Data.Mergeful.Item.ItemSyncResponse a)
+ Data.Mergeful.Item: instance Autodocodec.Class.HasCodec a => Autodocodec.Class.HasCodec (Data.Mergeful.Item.ServerItem a)
+ Data.Mergeful.Item: instance Autodocodec.Class.HasCodec a => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeful.Item.ClientItem a)
+ Data.Mergeful.Item: instance Autodocodec.Class.HasCodec a => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeful.Item.ItemSyncRequest a)
+ Data.Mergeful.Item: instance Autodocodec.Class.HasCodec a => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeful.Item.ItemSyncResponse a)
+ Data.Mergeful.Item: instance Autodocodec.Class.HasCodec a => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeful.Item.ServerItem a)
+ Data.Mergeful.Item: instance Autodocodec.Class.HasCodec a => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeful.Item.ClientItem a)
+ Data.Mergeful.Item: instance Autodocodec.Class.HasCodec a => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeful.Item.ItemSyncRequest a)
+ Data.Mergeful.Item: instance Autodocodec.Class.HasCodec a => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeful.Item.ItemSyncResponse a)
+ Data.Mergeful.Item: instance Autodocodec.Class.HasCodec a => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeful.Item.ServerItem a)
+ Data.Mergeful.Timed: instance Autodocodec.Class.HasCodec Data.Mergeful.Timed.ServerTime
+ Data.Mergeful.Timed: instance Autodocodec.Class.HasCodec a => Autodocodec.Class.HasCodec (Data.Mergeful.Timed.Timed a)
+ Data.Mergeful.Timed: instance Autodocodec.Class.HasCodec a => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeful.Timed.Timed a)
+ Data.Mergeful.Timed: instance Autodocodec.Class.HasCodec a => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeful.Timed.Timed a)
+ Data.Mergeful.Timed: timedObjectCodec :: HasCodec a => JSONObjectCodec (Timed a)
+ Data.Mergeful.Value: [clientValueChanged] :: ClientValue a -> !ChangedFlag
+ Data.Mergeful.Value: [clientValueTimedValue] :: ClientValue a -> !Timed a
+ Data.Mergeful.Value: [unServerValue] :: ServerValue a -> Timed a
+ Data.Mergeful.Value: instance Autodocodec.Class.HasCodec Data.Mergeful.Value.ChangedFlag
+ Data.Mergeful.Value: instance Autodocodec.Class.HasCodec a => Autodocodec.Class.HasCodec (Data.Mergeful.Value.ClientValue a)
+ Data.Mergeful.Value: instance Autodocodec.Class.HasCodec a => Autodocodec.Class.HasCodec (Data.Mergeful.Value.ServerValue a)
+ Data.Mergeful.Value: instance Autodocodec.Class.HasCodec a => Autodocodec.Class.HasCodec (Data.Mergeful.Value.ValueSyncRequest a)
+ Data.Mergeful.Value: instance Autodocodec.Class.HasCodec a => Autodocodec.Class.HasCodec (Data.Mergeful.Value.ValueSyncResponse a)
+ Data.Mergeful.Value: instance Autodocodec.Class.HasCodec a => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeful.Value.ClientValue a)
+ Data.Mergeful.Value: instance Autodocodec.Class.HasCodec a => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeful.Value.ServerValue a)
+ Data.Mergeful.Value: instance Autodocodec.Class.HasCodec a => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeful.Value.ValueSyncRequest a)
+ Data.Mergeful.Value: instance Autodocodec.Class.HasCodec a => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeful.Value.ValueSyncResponse a)
+ Data.Mergeful.Value: instance Autodocodec.Class.HasCodec a => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeful.Value.ClientValue a)
+ Data.Mergeful.Value: instance Autodocodec.Class.HasCodec a => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeful.Value.ServerValue a)
+ Data.Mergeful.Value: instance Autodocodec.Class.HasCodec a => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeful.Value.ValueSyncRequest a)
+ Data.Mergeful.Value: instance Autodocodec.Class.HasCodec a => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeful.Value.ValueSyncResponse a)
+ Data.Mergeful.Value: instance Data.Aeson.Types.FromJSON.FromJSON Data.Mergeful.Value.ChangedFlag
+ Data.Mergeful.Value: instance Data.Aeson.Types.ToJSON.ToJSON Data.Mergeful.Value.ChangedFlag
- Data.Mergeful.Collection: ClientAddition :: i -> ServerTime -> ClientAddition i
+ Data.Mergeful.Collection: ClientAddition :: !i -> !ServerTime -> ClientAddition i
- 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: ClientStore :: !Map ci a -> !Map si (Timed a) -> !Map si (Timed a) -> !Map si ServerTime -> ClientStore ci si a
- 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: 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 () -> !Set si -> m () -> !Set si -> m () -> !Map si (Timed a) -> m () -> !Map si (Timed a) -> m () -> !Map si (Timed a) -> m () -> !Map si (Timed a) -> m () -> !Map si (Timed a) -> m () -> !Set si -> m () -> ClientSyncProcessor ci si a (m :: Type -> Type)
- Data.Mergeful.Collection: ItemMergeStrategy :: (a -> a -> ChangeConflictResolution a) -> (a -> ClientDeletedConflictResolution) -> (a -> ServerDeletedConflictResolution) -> ItemMergeStrategy a
+ Data.Mergeful.Collection: ItemMergeStrategy :: !a -> a -> ChangeConflictResolution a -> !a -> ClientDeletedConflictResolution -> !a -> ServerDeletedConflictResolution -> ItemMergeStrategy 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: ServerSyncProcessor :: !m (Map si (Timed a)) -> !ci -> a -> m (Maybe si) -> !si -> ServerTime -> a -> m () -> !si -> m () -> ServerSyncProcessor ci si a m
- Data.Mergeful.Collection: [clientAdditionId] :: ClientAddition i -> i
+ Data.Mergeful.Collection: [clientAdditionId] :: ClientAddition i -> !i
- Data.Mergeful.Collection: [clientAdditionServerTime] :: ClientAddition i -> ServerTime
+ Data.Mergeful.Collection: [clientAdditionServerTime] :: ClientAddition i -> !ServerTime
- Data.Mergeful.Collection: [clientStoreAddedItems] :: ClientStore ci si a -> Map ci a
+ Data.Mergeful.Collection: [clientStoreAddedItems] :: ClientStore ci si a -> !Map ci a
- Data.Mergeful.Collection: [clientStoreDeletedItems] :: ClientStore ci si a -> Map si ServerTime
+ Data.Mergeful.Collection: [clientStoreDeletedItems] :: ClientStore ci si a -> !Map si ServerTime
- Data.Mergeful.Collection: [clientStoreSyncedButChangedItems] :: ClientStore ci si a -> Map si (Timed a)
+ Data.Mergeful.Collection: [clientStoreSyncedButChangedItems] :: ClientStore ci si a -> !Map si (Timed a)
- Data.Mergeful.Collection: [clientStoreSyncedItems] :: ClientStore ci si a -> Map si (Timed a)
+ Data.Mergeful.Collection: [clientStoreSyncedItems] :: ClientStore ci si a -> !Map si (Timed a)
- Data.Mergeful.Collection: [clientSyncProcessorQuerySyncedButChangedValues] :: ClientSyncProcessor ci si a -> !Set si -> m (Map si (Timed a))
+ Data.Mergeful.Collection: [clientSyncProcessorQuerySyncedButChangedValues] :: ClientSyncProcessor ci si a (m :: Type -> Type) -> !Set si -> m (Map si (Timed a))
- Data.Mergeful.Collection: [clientSyncProcessorSyncClientAdded] :: ClientSyncProcessor ci si a -> !Map ci (ClientAddition si) -> m ()
+ Data.Mergeful.Collection: [clientSyncProcessorSyncClientAdded] :: ClientSyncProcessor ci si a (m :: Type -> Type) -> !Map ci (ClientAddition si) -> m ()
- Data.Mergeful.Collection: [clientSyncProcessorSyncClientChanged] :: ClientSyncProcessor ci si a -> !Map si ServerTime -> m ()
+ Data.Mergeful.Collection: [clientSyncProcessorSyncClientChanged] :: ClientSyncProcessor ci si a (m :: Type -> Type) -> !Map si ServerTime -> m ()
- Data.Mergeful.Collection: [clientSyncProcessorSyncClientDeleted] :: ClientSyncProcessor ci si a -> !Set si -> m ()
+ Data.Mergeful.Collection: [clientSyncProcessorSyncClientDeleted] :: ClientSyncProcessor ci si a (m :: Type -> Type) -> !Set si -> m ()
- Data.Mergeful.Collection: [clientSyncProcessorSyncServerAdded] :: ClientSyncProcessor ci si a -> !Map si (Timed a) -> m ()
+ Data.Mergeful.Collection: [clientSyncProcessorSyncServerAdded] :: ClientSyncProcessor ci si a (m :: Type -> Type) -> !Map si (Timed a) -> m ()
- Data.Mergeful.Collection: [clientSyncProcessorSyncServerChanged] :: ClientSyncProcessor ci si a -> !Map si (Timed a) -> m ()
+ Data.Mergeful.Collection: [clientSyncProcessorSyncServerChanged] :: ClientSyncProcessor ci si a (m :: Type -> Type) -> !Map si (Timed a) -> m ()
- Data.Mergeful.Collection: [clientSyncProcessorSyncServerDeleted] :: ClientSyncProcessor ci si a -> !Set si -> m ()
+ Data.Mergeful.Collection: [clientSyncProcessorSyncServerDeleted] :: ClientSyncProcessor ci si a (m :: Type -> Type) -> !Set si -> m ()
- Data.Mergeful.Collection: [itemMergeStrategyMergeChangeConflict] :: ItemMergeStrategy a -> a -> a -> ChangeConflictResolution a
+ Data.Mergeful.Collection: [itemMergeStrategyMergeChangeConflict] :: ItemMergeStrategy a -> !a -> a -> ChangeConflictResolution a
- Data.Mergeful.Collection: [itemMergeStrategyMergeClientDeletedConflict] :: ItemMergeStrategy a -> a -> ClientDeletedConflictResolution
+ Data.Mergeful.Collection: [itemMergeStrategyMergeClientDeletedConflict] :: ItemMergeStrategy a -> !a -> ClientDeletedConflictResolution
- Data.Mergeful.Collection: [itemMergeStrategyMergeServerDeletedConflict] :: ItemMergeStrategy a -> a -> ServerDeletedConflictResolution
+ Data.Mergeful.Collection: [itemMergeStrategyMergeServerDeletedConflict] :: ItemMergeStrategy a -> !a -> ServerDeletedConflictResolution
- Data.Mergeful.Collection: [serverSyncProcessorAddItem] :: ServerSyncProcessor ci si a m -> !a -> m si
+ Data.Mergeful.Collection: [serverSyncProcessorAddItem] :: ServerSyncProcessor ci si a m -> !ci -> a -> m (Maybe si)
- Data.Mergeful.Collection: data ClientSyncProcessor ci si a (m :: * -> *)
+ Data.Mergeful.Collection: data ClientSyncProcessor ci si a (m :: Type -> Type)
- Data.Mergeful.Collection: mergeClientDeletedConflicts :: (a -> ClientDeletedConflictResolution) -> Map si (Timed a) -> Map si (Timed a)
+ Data.Mergeful.Collection: mergeClientDeletedConflicts :: (a -> ClientDeletedConflictResolution) -> Map si (Timed a) -> (Map si (Timed a), Map si (Timed a))
- Data.Mergeful.Collection: mergeServerDeletedConflicts :: (a -> ServerDeletedConflictResolution) -> Map si (Timed a) -> Set si
+ Data.Mergeful.Collection: mergeServerDeletedConflicts :: (a -> ServerDeletedConflictResolution) -> Map si (Timed a) -> (Set si, Set si)
- Data.Mergeful.Item: ItemMergeStrategy :: (a -> a -> ChangeConflictResolution a) -> (a -> ClientDeletedConflictResolution) -> (a -> ServerDeletedConflictResolution) -> ItemMergeStrategy a
+ Data.Mergeful.Item: ItemMergeStrategy :: !a -> a -> ChangeConflictResolution a -> !a -> ClientDeletedConflictResolution -> !a -> ServerDeletedConflictResolution -> ItemMergeStrategy a
- Data.Mergeful.Item: [itemMergeStrategyMergeChangeConflict] :: ItemMergeStrategy a -> a -> a -> ChangeConflictResolution a
+ Data.Mergeful.Item: [itemMergeStrategyMergeChangeConflict] :: ItemMergeStrategy a -> !a -> a -> ChangeConflictResolution a
- Data.Mergeful.Item: [itemMergeStrategyMergeClientDeletedConflict] :: ItemMergeStrategy a -> a -> ClientDeletedConflictResolution
+ Data.Mergeful.Item: [itemMergeStrategyMergeClientDeletedConflict] :: ItemMergeStrategy a -> !a -> ClientDeletedConflictResolution
- Data.Mergeful.Item: [itemMergeStrategyMergeServerDeletedConflict] :: ItemMergeStrategy a -> a -> ServerDeletedConflictResolution
+ Data.Mergeful.Item: [itemMergeStrategyMergeServerDeletedConflict] :: ItemMergeStrategy a -> !a -> ServerDeletedConflictResolution

Files

CHANGELOG.md view
@@ -1,6 +1,15 @@ # Changelog -## [Unreleased]+## [0.3.0.0] - 2022-06-19++### Added++* `autodocodec` support++### Changed++* Fixed a bug in which the `ValueSyncResponse` serialised conflicting values to JSON strangely.+* Fixed a bug in which the `ItemSyncResponseConflict` and `ItemSyncResponseConflict` serialised conflicting values to JSON strangely.  ## [0.2.0.0] - 2020-05-21 
LICENSE view
@@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018-2019 Tom Sydney Kerckhove+Copyright (c) 2018-2021 Tom Sydney Kerckhove  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal
− README.md
@@ -1,1 +0,0 @@-# mergeful
− Setup.hs
@@ -1,3 +0,0 @@-import Distribution.Simple--main = defaultMain
mergeful.cabal view
@@ -1,24 +1,23 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.33.0.+-- This file has been generated from package.yaml by hpack version 0.34.7. -- -- see: https://github.com/sol/hpack ----- hash: 5c04518a45f3748d42c829e645b69f607f23450b79fa96bcd6c567976b0465b7+-- hash: 257049459827bc46485c454fe83f602efd9c34332aceaeb318f90a161c06082a  name:           mergeful-version:        0.2.0.0+version:        0.3.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-2020 Tom Sydney Kerckhove+copyright:      Copyright: (c) 2018-2022 Tom Sydney Kerckhove license:        MIT license-file:   LICENSE build-type:     Simple extra-source-files:-    README.md     CHANGELOG.md  source-repository head@@ -39,6 +38,7 @@   ghc-options: -Wall -fwarn-redundant-constraints   build-depends:       aeson+    , autodocodec     , base >=4.7 && <5     , containers     , deepseq
src/Data/Mergeful/Collection.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingVia #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}@@ -58,7 +59,7 @@     changeItemInClientStore,     deleteItemFromClientStore, -    -- ** Maxing a sync request+    -- ** Making a sync request     SyncRequest (..),     initialSyncRequest,     makeSyncRequest,@@ -107,20 +108,20 @@   ) where +import Autodocodec import Control.Applicative import Control.DeepSeq import Control.Monad import Control.Monad.State-import Data.Aeson+import Data.Aeson (FromJSON, FromJSONKey (..), ToJSON, ToJSONKey (..))+import Data.Kind 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@@ -129,31 +130,35 @@ -- | 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)+newtype ClientId = ClientId+  { unClientId :: Word64+  }+  deriving stock (Show, Eq, Ord, Generic)+  deriving newtype (Enum, Bounded, ToJSONKey, FromJSONKey)+  deriving (FromJSON, ToJSON) via (Autodocodec ClientId)  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 HasCodec ClientId where+  codec = dimapCodec ClientId unClientId codec <?> "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 stock (Show, Eq, Generic)+  deriving (FromJSON, ToJSON) via (Autodocodec (ClientStore ci si a))+ instance   (Validity ci, Validity si, Show ci, Show si, Ord ci, Ord si, Validity a) =>   Validity (ClientStore ci si a)@@ -161,36 +166,36 @@   validate cs@ClientStore {..} =     mconcat       [ genericValidate cs,-        declare "There are no duplicate IDs"-          $ distinct-          $ concat-            [ M.keys clientStoreSyncedItems,-              M.keys clientStoreSyncedButChangedItems,-              M.keys clientStoreDeletedItems-            ]+        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)+  ( Ord ci,+    FromJSONKey ci,+    ToJSONKey ci,+    Ord si,+    FromJSONKey si,+    ToJSONKey si,+    Eq a,+    HasCodec a+  ) =>+  HasCodec (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-        ]+  codec =+    object "ClientStore" $+      ClientStore+        <$> optionalFieldWithOmittedDefault "added" M.empty "added items" .= clientStoreAddedItems+        <*> optionalFieldWithOmittedDefault "synced" M.empty "synced items" .= clientStoreSyncedItems+        <*> optionalFieldWithOmittedDefault "changed" M.empty "changed items" .= clientStoreSyncedButChangedItems+        <*> optionalFieldWithOmittedDefault "deleted" M.empty "deleted items" .= clientStoreDeletedItems  -- | A client store to start with. --@@ -335,38 +340,48 @@ 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)+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 stock (Show, Eq, Generic)+  deriving (FromJSON, ToJSON) via (Autodocodec (ServerStore si a))  instance (Validity si, Show si, Ord si, Validity a) => Validity (ServerStore si a)  instance (NFData si, NFData a) => NFData (ServerStore si a) +instance+  ( Ord si,+    FromJSONKey si,+    ToJSONKey si,+    HasCodec a+  ) =>+  HasCodec (ServerStore si a)+  where+  codec = dimapCodec ServerStore serverStoreItems codec+ -- | 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)+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 stock (Show, Eq, Generic)+  deriving (FromJSON, ToJSON) via (Autodocodec (SyncRequest ci si a))  instance   (Validity ci, Validity si, Show ci, Show si, Ord ci, Ord si, Validity a) =>@@ -375,36 +390,36 @@   validate sr@SyncRequest {..} =     mconcat       [ genericValidate sr,-        declare "There are no duplicate IDs"-          $ distinct-          $ concat-            [ M.keys syncRequestKnownItems,-              M.keys syncRequestKnownButChangedItems,-              M.keys syncRequestDeletedItems-            ]+        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)+  ( Ord ci,+    FromJSONKey ci,+    ToJSONKey ci,+    Ord si,+    FromJSONKey si,+    ToJSONKey si,+    Eq a,+    HasCodec a+  ) =>+  HasCodec (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-        ]+  codec =+    object "SyncRequest" $+      SyncRequest+        <$> optionalFieldWithOmittedDefault "added" M.empty "new items" .= syncRequestNewItems+        <*> optionalFieldWithOmittedDefault "synced" M.empty "known items" .= syncRequestKnownItems+        <*> optionalFieldWithOmittedDefault "changed" M.empty "known but changed items" .= syncRequestKnownButChangedItems+        <*> optionalFieldWithOmittedDefault "deleted" M.empty "deleted items" .= syncRequestDeletedItems  -- | An intial 'SyncRequest' to start with. --@@ -418,66 +433,67 @@       syncRequestDeletedItems = M.empty     } -data ClientAddition i-  = ClientAddition-      { clientAdditionId :: i,-        clientAdditionServerTime :: ServerTime-      }-  deriving (Show, Eq, Generic)+data ClientAddition i = ClientAddition+  { clientAdditionId :: !i,+    clientAdditionServerTime :: !ServerTime+  }+  deriving stock (Show, Eq, Generic)+  deriving (FromJSON, ToJSON) via (Autodocodec (ClientAddition i))  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]+instance HasCodec i => HasCodec (ClientAddition i) where+  codec =+    object "ClientAddition" $+      ClientAddition+        <$> requiredField "id" "client-side identifier" .= clientAdditionId+        <*> requiredField "time" "server-side 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)+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 stock (Show, Eq, Generic)+  deriving (FromJSON, ToJSON) via (Autodocodec (SyncResponse ci si a))  instance   (Validity ci, Validity si, Show ci, Show si, Ord ci, Ord si, Validity a) =>@@ -486,55 +502,49 @@   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-            ]+        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)+  ( Ord ci,+    FromJSONKey ci,+    ToJSONKey ci,+    HasCodec ci,+    Ord si,+    FromJSONKey si,+    ToJSONKey si,+    HasCodec si,+    Eq a,+    HasCodec a+  ) =>+  HasCodec (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-        ]+  codec =+    object "SyncResponse" $+      SyncResponse+        <$> optionalFieldWithOmittedDefault "client-added" M.empty "items added by the client" .= syncResponseClientAdded+        <*> optionalFieldWithOmittedDefault "client-changed" M.empty "items changed by the client" .= syncResponseClientChanged+        <*> optionalFieldWithOmittedDefault "client-deleted" S.empty "items deleted by the client" .= syncResponseClientDeleted+        <*> optionalFieldWithOmittedDefault "server-added" M.empty "items added by the server" .= syncResponseServerAdded+        <*> optionalFieldWithOmittedDefault "server-changed" M.empty "items changed by the server" .= syncResponseServerChanged+        <*> optionalFieldWithOmittedDefault "server-deleted" S.empty "items deleted by the server" .= syncResponseServerDeleted+        <*> optionalFieldWithOmittedDefault "conflict" M.empty "items that were changed simultaneously" .= syncResponseConflicts+        <*> optionalFieldWithOmittedDefault "conflict-client-deleted" M.empty "items that the server changed while the client deleted it" .= syncResponseConflictsClientDeleted+        <*> optionalFieldWithOmittedDefault "conflict-server-deleted" S.empty "items that the client changed while the server deleted it" .= syncResponseConflictsServerDeleted  -- | A sync response to start with. --@@ -553,12 +563,6 @@       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.@@ -637,12 +641,32 @@               let leftovers = mergeDeletedItems (clientStoreDeletedItems cs) s                in cs {clientStoreDeletedItems = leftovers}           ),-      clientSyncProcessorSyncMergedConflict = \resolved ->+      clientSyncProcessorSyncChangeConflictKeepLocal = \_ -> pure (),+      clientSyncProcessorSyncChangeConflictMerged = \resolved ->         modify           ( \cs ->               let newSyncedButChanged = M.union resolved (clientStoreSyncedButChangedItems cs)                in cs {clientStoreSyncedButChangedItems = newSyncedButChanged, clientStoreSyncedItems = clientStoreSyncedItems cs `M.difference` newSyncedButChanged}           ),+      clientSyncProcessorSyncChangeConflictTakeRemote = \m ->+        modify+          ( \cs ->+              let newSynced = m `M.union` clientStoreSyncedItems cs+               in cs {clientStoreSyncedItems = newSynced, clientStoreSyncedButChangedItems = clientStoreSyncedButChangedItems cs `M.difference` newSynced}+          ),+      clientSyncProcessorSyncClientDeletedConflictTakeRemoteChanged = \m ->+        modify (\cs -> cs {clientStoreSyncedItems = m `M.union` clientStoreSyncedItems cs}),+      clientSyncProcessorSyncClientDeletedConflictStayDeleted = \_ -> pure (),+      clientSyncProcessorSyncServerDeletedConflictKeepLocalChange = \_ -> pure (),+      clientSyncProcessorSyncServerDeletedConflictDelete = \s ->+        modify+          ( \cs ->+              let m = M.fromSet (const ()) s+               in cs+                    { clientStoreSyncedItems = clientStoreSyncedItems cs `M.difference` m,+                      clientStoreSyncedButChangedItems = clientStoreSyncedButChangedItems cs `M.difference` m+                    }+          ),       clientSyncProcessorSyncServerAdded = \m ->         modify (\cs -> cs {clientStoreSyncedItems = m `M.union` clientStoreSyncedItems cs}),       clientSyncProcessorSyncServerChanged = \m ->@@ -702,44 +726,113 @@ 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 ())-      }+-- | A processor for dealing with @SyncResponse@s on the client side.+--+-- It has to deal with each of the 13 cases:+--+-- - server+--+--     - added+--     - changed+--     - deleted+--+-- - client+--+--     - added+--     - changed+--     - deleted+--+-- - client-deleted conflict+--+--     - take remote+--     - delete+--+-- - server-deleted conflict+--+--     - delete+--     - keep local+--+-- - change conflict+--+--     - take remote+--     - merge+--     - keep local+--+-- It is a lot of work to implement one of these, so make sure to have a look at the mergeful companion packages to see if maybe there is already one for your application domain.+data ClientSyncProcessor ci si a (m :: Type -> Type) = 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 ()),+    -- | Re-create the items that need to be created locally as a result of a 'client deleted' conflict that has been merged by taking the remote value.+    --+    -- You can likely implement this in the same way as @clientSyncProcessorSyncServerAdded@.+    clientSyncProcessorSyncClientDeletedConflictTakeRemoteChanged :: !(Map si (Timed a) -> m ()),+    -- | Leave the items deleted that need to be left deleted as a result of a 'client deleted' conflict that has been merged by leaving it deleted.+    --+    -- You likely don't have to do anything with these, as they are the way that has been decided they should be, but you may want to log them or so.+    clientSyncProcessorSyncClientDeletedConflictStayDeleted :: !(Map si (Timed a) -> m ()),+    -- | Leave the items undeleted that need to be left deleted as a result of a 'server deleted' conflict that has been merged by leaving it undeleted.+    --+    -- You likely don't have to do anything with these, as they are the way that has been decided they should be, but you may want to log them or so.+    clientSyncProcessorSyncServerDeletedConflictKeepLocalChange :: !(Set si -> m ()),+    -- | Delete the items that need to be deleted locally as a result of a 'server deleted' conflict that has been merged by deleting the local value.+    --+    -- You can likely implement this in the same way as @clientSyncProcessorSyncServerDeleted@.+    clientSyncProcessorSyncServerDeletedConflictDelete :: !(Set si -> m ()),+    -- | Deal with the items for which no conflict was resolved.+    --+    -- You likely don't have to do anything with these, as they are the way that has been decided they should be, but you may want to log them or so.+    clientSyncProcessorSyncChangeConflictKeepLocal :: !(Map si (Timed a) -> m ()),+    -- | Store the items that were in a conflict but the conflict was resolved correctly.+    -- These items should be marked as changed.+    clientSyncProcessorSyncChangeConflictMerged :: !(Map si (Timed a) -> m ()),+    -- | Store the items that were in a conflict but the client will take the remote values+    -- These items should be marked as unchanged.+    --+    -- You can likely implement this in the same way as @clientSyncProcessorSyncServerChanged@.+    clientSyncProcessorSyncChangeConflictTakeRemote :: !(Map si (Timed a) -> m ()),+    -- | Store the items that the server added+    clientSyncProcessorSyncServerAdded :: !(Map si (Timed a) -> m ()),+    -- | Store the items that the server changed+    clientSyncProcessorSyncServerChanged :: !(Map si (Timed a) -> m ()),+    -- | Store the items that the server deleted+    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+  let (remoteChangeToTake, remoteChangesToIgnore) = 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+  let (unresolvedChangeConflicts, 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+  let (localChangesToDelete, localChangesToBeKept) = mergeServerDeletedConflicts itemMergeStrategyMergeServerDeletedConflict clientServerDeletedConflicts   -- The order here matters.-  clientSyncProcessorSyncServerAdded $ M.union syncResponseServerAdded resolvedClientDeletedConflicts-  clientSyncProcessorSyncServerChanged $ M.union syncResponseServerChanged resolvedChangeConflicts-  clientSyncProcessorSyncServerDeleted $ S.union syncResponseServerDeleted resolvedServerDeletedConflicts-  clientSyncProcessorSyncMergedConflict mergedChangeConflicts+  clientSyncProcessorSyncServerAdded syncResponseServerAdded+  clientSyncProcessorSyncServerChanged syncResponseServerChanged+  clientSyncProcessorSyncServerDeleted syncResponseServerDeleted+  clientSyncProcessorSyncChangeConflictTakeRemote resolvedChangeConflicts+  clientSyncProcessorSyncChangeConflictMerged mergedChangeConflicts+  clientSyncProcessorSyncChangeConflictKeepLocal unresolvedChangeConflicts+  clientSyncProcessorSyncClientDeletedConflictTakeRemoteChanged remoteChangeToTake+  clientSyncProcessorSyncClientDeletedConflictStayDeleted remoteChangesToIgnore+  clientSyncProcessorSyncServerDeletedConflictDelete localChangesToDelete+  clientSyncProcessorSyncServerDeletedConflictKeepLocalChange localChangesToBeKept   clientSyncProcessorSyncClientDeleted syncResponseClientDeleted   clientSyncProcessorSyncClientChanged syncResponseClientChanged   clientSyncProcessorSyncClientAdded syncResponseClientAdded@@ -783,8 +876,8 @@   -- | 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 _) ->+  (Map si (Timed a), Map si (Timed a))+mergeClientDeletedConflicts func = M.partition $ \(Timed si _) ->   case func si of     TakeRemoteChange -> True     StayDeleted -> False@@ -795,31 +888,34 @@   -- | 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+  (Set si, Set si)+mergeServerDeletedConflicts func m = both M.keysSet $+  flip M.partition m $ \(Timed si _) -> case func si of+    KeepLocalChange -> False+    Delete -> True+  where+    both :: (a -> b) -> (a, a) -> (b, b)+    both f (a1, a2) = (f a1, f a2)  data Identifier ci si-  = OnlyServer si-  | BothServerAndClient si ci+  = 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 ())-      }+data ServerSyncProcessor ci si a m = ServerSyncProcessor+  { -- | Read all items+    serverSyncProcessorRead :: !(m (Map si (Timed a))),+    -- | Add an item with 'initialServerTime', can fail.+    serverSyncProcessorAddItem :: !(ci -> a -> m (Maybe si)),+    -- | Update an item+    serverSyncProcessorChangeItem :: !(si -> ServerTime -> a -> m ()),+    -- | Delete an item+    serverSyncProcessorDeleteItem :: !(si -> m ())+  }   deriving (Generic)  -- | Process a server sync@@ -872,9 +968,10 @@ 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}+  syncResponseClientAdded <- fmap (M.mapMaybe id) $+    flip M.traverseWithKey syncRequestNewItems $ \cid a -> do+      msi <- serverSyncProcessorAddItem cid a+      pure $ (\si -> ClientAddition {clientAdditionId = si, clientAdditionServerTime = initialServerTime}) <$> (msi :: Maybe si)   -- C:   let decideOnSynced tup@(sc, sd) (si, ct) =         case M.lookup si serverItems of@@ -945,10 +1042,10 @@ pureServerSyncProcessor genId = ServerSyncProcessor {..}   where     serverSyncProcessorRead = gets serverStoreItems-    serverSyncProcessorAddItem a = do+    serverSyncProcessorAddItem _ a = do       i <- lift genId       modify (\(ServerStore m) -> ServerStore (M.insert i (Timed a initialServerTime) m))-      pure i+      pure (Just i) -- Always succeed     serverSyncProcessorChangeItem si st a =       modify         ( \(ServerStore m) ->
src/Data/Mergeful/Item.hs view
@@ -1,5 +1,7 @@ {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingVia #-} {-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} @@ -72,10 +74,11 @@   ) where -import Control.Applicative+import Autodocodec import Control.DeepSeq-import Data.Aeson as JSON+import Data.Aeson (FromJSON, ToJSON) import Data.Mergeful.Timed+import Data.Text (Text) import Data.Validity import GHC.Generics (Generic) @@ -90,35 +93,45 @@     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)+  deriving stock (Show, Eq, Generic)+  deriving (FromJSON, ToJSON) via (Autodocodec (ClientItem a))  instance Validity a => Validity (ClientItem a)  instance NFData a => NFData (ClientItem a) -instance FromJSON a => FromJSON (ClientItem a) where-  parseJSON =-    withObject "ClientItem" $ \o -> do-      typ <- o .: "type"-      case typ :: String of-        "empty" -> pure ClientEmpty-        "added" -> ClientAdded <$> o .: "value"-        "synced" -> ClientItemSynced <$> (Timed <$> o .: "value" <*> o .: "time")-        "changed" -> ClientItemSyncedButChanged <$> (Timed <$> o .: "value" <*> o .: "time")-        "deleted" -> ClientDeleted <$> o .: "time"-        _ -> fail "unknown item type"+instance HasCodec a => HasCodec (ClientItem a) where+  codec =+    object "ClientItem" $+      dimapCodec f g $+        disjointEitherCodec+          ( disjointEitherCodec+              (typeField "empty" <*> pure ())+              (typeField "added" <*> requiredField "value" "item that was added, client-side")+          )+          ( disjointEitherCodec+              (typeField "synced" <*> timedObjectCodec)+              ( disjointEitherCodec+                  (typeField "changed" <*> timedObjectCodec)+                  (typeField "deleted" <*> requiredField "time" "last time the server confirmed a change, from the client's perspective")+              )+          )+    where+      f = \case+        Left (Left ()) -> ClientEmpty+        Left (Right v) -> ClientAdded v+        Right (Left tv) -> ClientItemSynced tv+        Right (Right (Left tv)) -> ClientItemSyncedButChanged tv+        Right (Right (Right st)) -> ClientDeleted st+      g = \case+        ClientEmpty -> Left (Left ())+        ClientAdded v -> Left (Right v)+        ClientItemSynced tv -> Right (Left tv)+        ClientItemSyncedButChanged tv -> Right (Right (Left tv))+        ClientDeleted st -> Right (Right (Right st)) -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]+      typeField :: Text -> ObjectCodec b (a -> a)+      typeField typeName = id <$ requiredFieldWith' "type" (literalTextCodec typeName) .= const typeName  -- | A client item to start with. --@@ -131,23 +144,24 @@     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)+  deriving stock (Show, Eq, Generic)+  deriving (FromJSON, ToJSON) via (Autodocodec (ServerItem a))  instance Validity a => Validity (ServerItem a)  instance NFData a => NFData (ServerItem a) -instance FromJSON a => FromJSON (ServerItem a) where-  parseJSON =-    withObject "ServerItem" $ \o ->-      ServerFull <$> (Timed <$> o .: "value" <*> o .: "time") <|> pure ServerEmpty--instance ToJSON a => ToJSON (ServerItem a) where-  toJSON si =-    object $-      case si of-        ServerEmpty -> []-        ServerFull Timed {..} -> ["value" .= timedValue, "time" .= timedTime]+instance HasCodec a => HasCodec (ServerItem a) where+  codec =+    object "ServerItem" $+      dimapCodec f g $ possiblyJointEitherCodec timedObjectCodec (pure ())+    where+      f = \case+        Left tv -> ServerFull tv+        Right () -> ServerEmpty+      g = \case+        ServerFull tv -> Left tv+        ServerEmpty -> Right ()  -- | A server item to start with. --@@ -168,36 +182,45 @@   | -- | 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)+  deriving stock (Show, Eq, Generic)+  deriving (FromJSON, ToJSON) via (Autodocodec (ItemSyncRequest a))  instance Validity a => Validity (ItemSyncRequest a)  instance NFData a => NFData (ItemSyncRequest a) -instance FromJSON a => FromJSON (ItemSyncRequest a) where-  parseJSON =-    withObject "ItemSyncRequest" $ \o -> do-      typ <- o .: "type"-      case typ :: String of-        "empty" -> pure ItemSyncRequestPoll-        "added" -> ItemSyncRequestNew <$> o .: "value"-        "synced" -> ItemSyncRequestKnown <$> o .: "time"-        "changed" -> ItemSyncRequestKnownButChanged <$> (Timed <$> o .: "value" <*> o .: "time")-        "deleted" -> ItemSyncRequestDeletedLocally <$> o .: "time"-        _ -> fail "unknown item type"+instance HasCodec a => HasCodec (ItemSyncRequest a) where+  codec =+    object "ItemSyncRequest" $+      dimapCodec f g $+        disjointEitherCodec+          ( disjointEitherCodec+              (typeField "empty" <*> pure ())+              (typeField "added" <*> requiredField "value" "item that was added, client-side")+          )+          ( disjointEitherCodec+              (typeField "synced" <*> requiredField "time" "last time the server confirmed a change, from the client's perspective")+              ( disjointEitherCodec+                  (typeField "changed" <*> timedObjectCodec)+                  (typeField "deleted" <*> requiredField "time" "last time the server confirmed a change, from the client's perspective")+              )+          )+    where+      f = \case+        Left (Left ()) -> ItemSyncRequestPoll+        Left (Right v) -> ItemSyncRequestNew v+        Right (Left tv) -> ItemSyncRequestKnown tv+        Right (Right (Left tv)) -> ItemSyncRequestKnownButChanged tv+        Right (Right (Right st)) -> ItemSyncRequestDeletedLocally st+      g = \case+        ItemSyncRequestPoll -> Left (Left ())+        ItemSyncRequestNew v -> Left (Right v)+        ItemSyncRequestKnown tv -> Right (Left tv)+        ItemSyncRequestKnownButChanged tv -> Right (Right (Left tv))+        ItemSyncRequestDeletedLocally st -> Right (Right (Right st)) -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]+      typeField :: Text -> ObjectCodec b (a -> a)+      typeField typeName = id <$ requiredFieldWith' "type" (literalTextCodec typeName) .= const typeName  -- | An intial 'ItemSyncRequest' to start with. --@@ -253,50 +276,75 @@     -- or deal with the conflict somehow, and then try to re-sync.     ItemSyncResponseConflictClientDeleted !(Timed a)   | ItemSyncResponseConflictServerDeleted-  deriving (Show, Eq, Generic)+  deriving stock (Show, Eq, Generic)+  deriving (FromJSON, ToJSON) via (Autodocodec (ItemSyncResponse a))  instance Validity a => Validity (ItemSyncResponse a)  instance NFData a => NFData (ItemSyncResponse a) -instance FromJSON a => FromJSON (ItemSyncResponse a) where-  parseJSON =-    withObject "ItemSyncResponse" $ \o -> do-      typ <- o .: "type"-      case typ :: String of-        "in-sync-empty" -> pure ItemSyncResponseInSyncEmpty-        "in-sync-full" -> pure ItemSyncResponseInSyncFull-        "client-added" -> ItemSyncResponseClientAdded <$> o .: "time"-        "client-changed" -> ItemSyncResponseClientChanged <$> o .: "time"-        "client-deleted" -> pure ItemSyncResponseClientDeleted-        "server-added" -> ItemSyncResponseServerAdded <$> (Timed <$> o .: "value" <*> o .: "time")-        "server-changed" ->-          ItemSyncResponseServerChanged <$> (Timed <$> o .: "value" <*> o .: "time")-        "server-deleted" -> pure ItemSyncResponseServerDeleted-        "conflict" -> ItemSyncResponseConflict <$> o .: "value"-        "conflict-client-deleted" -> ItemSyncResponseConflictClientDeleted <$> o .: "value"-        "conflict-server-deleted" -> pure ItemSyncResponseConflictServerDeleted-        _ -> fail "unknown type"+instance HasCodec a => HasCodec (ItemSyncResponse a) where+  codec =+    object "ItemSyncResponse" $+      dimapCodec f g $+        disjointEitherCodec+          ( disjointEitherCodec+              ( disjointEitherCodec+                  (typeField "in-sync-empty" <*> pure ())+                  (typeField "in-sync-full" <*> pure ())+              )+              ( disjointEitherCodec+                  (typeField "client-added" <*> requiredField "time" "server's confirmation of the addition")+                  (typeField "client-changed" <*> requiredField "time" "server's confirmation of the addition")+              )+          )+          ( disjointEitherCodec+              ( disjointEitherCodec+                  ( disjointEitherCodec+                      (typeField "client-deleted" <*> pure ())+                      (typeField "server-added" <*> timedObjectCodec)+                  )+                  ( disjointEitherCodec+                      (typeField "server-changed" <*> timedObjectCodec)+                      (typeField "server-deleted" <*> pure ())+                  )+              )+              ( disjointEitherCodec+                  ( disjointEitherCodec+                      (typeField "conflict" <*> timedObjectCodec)+                      (typeField "conflict-client-deleted" <*> timedObjectCodec)+                  )+                  (typeField "conflict-server-deleted" <*> pure ())+              )+          )+    where+      f = \case+        Left (Left (Left ())) -> ItemSyncResponseInSyncEmpty+        Left (Left (Right ())) -> ItemSyncResponseInSyncFull+        Left (Right (Left st)) -> ItemSyncResponseClientAdded st+        Left (Right (Right st)) -> ItemSyncResponseClientChanged st+        Right (Left (Left (Left ()))) -> ItemSyncResponseClientDeleted+        Right (Left (Left (Right tv))) -> ItemSyncResponseServerAdded tv+        Right (Left (Right (Left tv))) -> ItemSyncResponseServerChanged tv+        Right (Left (Right (Right ()))) -> ItemSyncResponseServerDeleted+        Right (Right (Left (Left tv))) -> ItemSyncResponseConflict tv+        Right (Right (Left (Right tv))) -> ItemSyncResponseConflictClientDeleted tv+        Right (Right (Right ())) -> ItemSyncResponseConflictServerDeleted+      g = \case+        ItemSyncResponseInSyncEmpty -> Left (Left (Left ()))+        ItemSyncResponseInSyncFull -> Left (Left (Right ()))+        ItemSyncResponseClientAdded st -> Left (Right (Left st))+        ItemSyncResponseClientChanged st -> Left (Right (Right st))+        ItemSyncResponseClientDeleted -> Right (Left (Left (Left ())))+        ItemSyncResponseServerAdded tv -> Right (Left (Left (Right tv)))+        ItemSyncResponseServerChanged tv -> Right (Left (Right (Left tv)))+        ItemSyncResponseServerDeleted -> Right (Left (Right (Right ())))+        ItemSyncResponseConflict tv -> Right (Right (Left (Left tv)))+        ItemSyncResponseConflictClientDeleted tv -> Right (Right (Left (Right tv)))+        ItemSyncResponseConflictServerDeleted -> Right (Right (Right ())) -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"+      typeField :: Text -> ObjectCodec b (a -> a)+      typeField typeName = id <$ requiredFieldWith' "type" (literalTextCodec typeName) .= const typeName  -- | Produce an 'ItemSyncRequest' from a 'ClientItem'. --@@ -379,17 +427,16 @@ mergeItemSyncResponseUsingCRDT = mergeItemSyncResponseUsingStrategy . mergeUsingCRDTStrategy  -- | A strategy to merge conflicts for item synchronisation-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-      }+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)  data ChangeConflictResolution a@@ -548,13 +595,13 @@               (ItemSyncResponseConflict t, store)             ItemSyncRequestKnown ct ->               if ct >= st-                then-- 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.)                   (ItemSyncResponseInSyncFull, store)-                else-- The client time is less than the server time+                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.@@ -565,25 +612,25 @@                   )             ItemSyncRequestKnownButChanged Timed {timedValue = ci, timedTime = ct} ->               if ct >= st-                then-- 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.                    ( ItemSyncResponseClientChanged st',                     ServerFull (Timed {timedValue = ci, timedTime = st'})                   )-                else-- The client time is less than the server time+                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.                   (ItemSyncResponseConflict t, store)             ItemSyncRequestDeletedLocally ct ->               if ct >= st-                then-- 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.                   (ItemSyncResponseClientDeleted, ServerEmpty)-                else-- The client time is less than the server time+                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.
src/Data/Mergeful/Timed.hs view
@@ -1,7 +1,6 @@ {-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE DerivingVia #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}  -- | Dealing with server times. --@@ -11,11 +10,13 @@     initialServerTime,     incrementServerTime,     Timed (..),+    timedObjectCodec,   ) where +import Autodocodec import Control.DeepSeq-import Data.Aeson as JSON+import Data.Aeson (FromJSON, ToJSON) import Data.Validity import Data.Word import GHC.Generics (Generic)@@ -32,16 +33,19 @@ -- 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)+newtype ServerTime = ServerTime+  { unServerTime :: Word64+  }+  deriving stock (Show, Eq, Ord, Generic)+  deriving (FromJSON, ToJSON) via (Autodocodec ServerTime)  instance Validity ServerTime  instance NFData ServerTime +instance HasCodec ServerTime where+  codec = dimapCodec ServerTime unServerTime codec <?> "Server time"+ -- | A server time to start with. initialServerTime :: ServerTime initialServerTime = ServerTime 0@@ -51,19 +55,22 @@ incrementServerTime (ServerTime w) = ServerTime (succ w)  -- | A value along with a server time.-data Timed a-  = Timed-      { timedValue :: !a,-        timedTime :: !ServerTime-      }-  deriving (Show, Eq, Generic)+data Timed a = Timed+  { timedValue :: !a,+    timedTime :: !ServerTime+  }+  deriving stock (Show, Eq, Generic)+  deriving (FromJSON, ToJSON) via (Autodocodec (Timed a))  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"+instance HasCodec a => HasCodec (Timed a) where+  codec = object "Timed" timedObjectCodec -instance ToJSON a => ToJSON (Timed a) where-  toJSON Timed {..} = object ["value" .= timedValue, "time" .= timedTime]+timedObjectCodec :: HasCodec a => JSONObjectCodec (Timed a)+timedObjectCodec =+  Timed+    <$> requiredField "value" "timed value" .= timedValue+    <*> requiredField "time" "timed time" .= timedTime
src/Data/Mergeful/Value.hs view
@@ -1,7 +1,8 @@ {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingVia #-} {-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}  -- | A way to synchronise a single value with safe merge conflicts. --@@ -60,21 +61,34 @@   ) where +import Autodocodec import Control.DeepSeq-import Data.Aeson as JSON+import Data.Aeson (FromJSON, ToJSON) import Data.Mergeful.Timed+import Data.Text (Text) import Data.Validity import GHC.Generics (Generic)  data ChangedFlag   = Changed   | NotChanged-  deriving (Show, Eq, Generic)+  deriving stock (Show, Eq, Generic)+  deriving (FromJSON, ToJSON) via (Autodocodec ChangedFlag)  instance Validity ChangedFlag  instance NFData ChangedFlag +instance HasCodec ChangedFlag where+  codec = dimapCodec f g codec+    where+      f = \case+        True -> Changed+        False -> NotChanged+      g = \case+        Changed -> True+        NotChanged -> False+ -- | The client side value. -- -- The only differences between `a` and 'ClientValue a' are that@@ -82,37 +96,23 @@ -- 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-  deriving (Show, Eq, Generic)+data ClientValue a = ClientValue+  { clientValueTimedValue :: !(Timed a),+    clientValueChanged :: !ChangedFlag+  }+  deriving stock (Show, Eq, Generic)+  deriving (FromJSON, ToJSON) via (Autodocodec (ClientValue a))  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"-            )--instance ToJSON a => ToJSON (ClientValue a) where-  toJSON (ClientValue Timed {..} cf) =-    object-      [ "value" .= timedValue,-        "time" .= timedTime,-        "changed"-          .= ( case cf of-                 Changed -> True-                 NotChanged -> False-             )-      ]+instance HasCodec a => HasCodec (ClientValue a) where+  codec =+    object "ClientValue" $+      ClientValue+        <$> timedObjectCodec .= clientValueTimedValue+        <*> requiredField "changed" "whether the value has changed, client-side" .= clientValueChanged  -- | Produce a client value based on an initial synchronisation request initialClientValue :: Timed a -> ClientValue a@@ -122,20 +122,16 @@ -- -- 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)-  deriving (Show, Eq, Generic)+newtype ServerValue a = ServerValue {unServerValue :: Timed a}+  deriving stock (Show, Eq, Generic)+  deriving (FromJSON, ToJSON) via (Autodocodec (ServerValue a))  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")--instance ToJSON a => ToJSON (ServerValue a) where-  toJSON (ServerValue Timed {..}) = object ["value" .= timedValue, "time" .= timedTime]+instance HasCodec a => HasCodec (ServerValue a) where+  codec = object "ServerValue" $ ServerValue <$> timedObjectCodec .= unServerValue  -- | Initialise a server value. --@@ -149,29 +145,30 @@   | -- | 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)+  deriving stock (Show, Eq, Generic)+  deriving (FromJSON, ToJSON) via (Autodocodec (ValueSyncRequest a))  instance Validity a => Validity (ValueSyncRequest a)  instance NFData a => NFData (ValueSyncRequest a) -instance FromJSON a => FromJSON (ValueSyncRequest a) where-  parseJSON =-    withObject "ValueSyncRequest" $ \o -> do-      typ <- o .: "type"-      case typ :: String of-        "synced" -> ValueSyncRequestKnown <$> o .: "time"-        "changed" -> ValueSyncRequestKnownButChanged <$> (Timed <$> o .: "value" <*> o .: "time")-        _ -> fail "unknown item type"+instance HasCodec a => HasCodec (ValueSyncRequest a) where+  codec =+    object "ValueSyncRequest" $+      dimapCodec f g $+        disjointEitherCodec+          (typeField "synced" <*> requiredField "time" "time at which the server said the value was last synced")+          (typeField "changed" <*> timedObjectCodec)+    where+      f = \case+        Left st -> ValueSyncRequestKnown st+        Right tv -> ValueSyncRequestKnownButChanged tv+      g = \case+        ValueSyncRequestKnown st -> Left st+        ValueSyncRequestKnownButChanged tv -> Right tv -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]+      typeField :: Text -> ObjectCodec b (a -> a)+      typeField typeName = id <$ requiredFieldWith' "type" (literalTextCodec typeName) .= const typeName  data ValueSyncResponse a   = -- | The client and server are fully in sync.@@ -188,35 +185,40 @@     ValueSyncResponseServerChanged !(Timed a)   | -- | The item at the server side     ValueSyncResponseConflict !(Timed a)-  deriving (Show, Eq, Generic)+  deriving stock (Show, Eq, Generic)+  deriving (FromJSON, ToJSON) via (Autodocodec (ValueSyncResponse a))  instance Validity a => Validity (ValueSyncResponse a)  instance NFData a => NFData (ValueSyncResponse a) -instance FromJSON a => FromJSON (ValueSyncResponse a) where-  parseJSON =-    withObject "ValueSyncResponse" $ \o -> do-      typ <- o .: "type"-      case typ :: String of-        "in-sync" -> pure ValueSyncResponseInSync-        "client-changed" -> ValueSyncResponseClientChanged <$> o .: "time"-        "server-changed" ->-          ValueSyncResponseServerChanged <$> (Timed <$> o .: "value" <*> o .: "time")-        "conflict" -> ValueSyncResponseConflict <$> o .: "value"-        _ -> fail "unknown type"+instance HasCodec a => HasCodec (ValueSyncResponse a) where+  codec =+    object "ValueSyncResponse" $+      dimapCodec f g $+        disjointEitherCodec+          ( disjointEitherCodec+              (typeField "in-sync" <*> pure ())+              (typeField "client-changed" <*> requiredField "time" "server time")+          )+          ( disjointEitherCodec+              (typeField "server-changed" <*> timedObjectCodec)+              (typeField "conflict" <*> timedObjectCodec)+          )+    where+      f = \case+        Left (Left ()) -> ValueSyncResponseInSync+        Left (Right st) -> ValueSyncResponseClientChanged st+        Right (Left tv) -> ValueSyncResponseServerChanged tv+        Right (Right tv) -> ValueSyncResponseConflict tv+      g = \case+        ValueSyncResponseInSync -> Left (Left ())+        ValueSyncResponseClientChanged st -> Left (Right st)+        ValueSyncResponseServerChanged tv -> Right (Left tv)+        ValueSyncResponseConflict tv -> Right (Right tv) -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]+      typeField :: Text -> ObjectCodec b (a -> a)+      typeField typeName = id <$ requiredFieldWith' "type" (literalTextCodec typeName) .= const typeName  -- | Produce an 'ItemSyncRequest' from a 'ClientItem'. --@@ -322,13 +324,13 @@   case sr of     ValueSyncRequestKnown ct ->       if ct >= st-        then-- 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.)           (ValueSyncResponseInSync, sv)-        else-- The client time is less than the server time+        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.@@ -336,7 +338,7 @@           (ValueSyncResponseServerChanged t, sv)     ValueSyncRequestKnownButChanged Timed {timedValue = ci, timedTime = ct} ->       if ct >= st-        then-- 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. @@ -344,7 +346,7 @@            in ( ValueSyncResponseClientChanged st',                 ServerValue (Timed {timedValue = ci, timedTime = st'})               )-        else-- The client time is less than the server time+        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.