packages feed

mergeless 0.0.0.0 → 0.1.0.0

raw patch · 2 files changed

+223/−124 lines, 2 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Data.Mergeless: instance (Data.Aeson.Types.FromJSON.FromJSON a, Data.Aeson.Types.FromJSON.FromJSON i, GHC.Classes.Ord a, GHC.Classes.Ord i) => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeless.Store i a)
- Data.Mergeless: instance (Data.Aeson.Types.FromJSON.FromJSON a, GHC.Classes.Ord i, Data.Aeson.Types.FromJSON.FromJSONKey i) => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeless.CentralStore i a)
- Data.Mergeless: instance (Data.Aeson.Types.ToJSON.ToJSON a, Data.Aeson.Types.ToJSON.ToJSON i) => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeless.Store i a)
- Data.Mergeless: instance (Data.Aeson.Types.ToJSON.ToJSONKey i, Data.Aeson.Types.ToJSON.ToJSON a) => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeless.CentralStore i a)
- Data.Mergeless: instance (GHC.Classes.Eq a, GHC.Classes.Eq i) => GHC.Classes.Eq (Data.Mergeless.CentralStore i a)
- Data.Mergeless: instance (GHC.Classes.Eq a, GHC.Classes.Eq i) => GHC.Classes.Eq (Data.Mergeless.Synced i a)
- Data.Mergeless: instance (GHC.Classes.Eq i, GHC.Classes.Eq a) => GHC.Classes.Eq (Data.Mergeless.StoreItem i a)
- Data.Mergeless: instance (GHC.Classes.Eq i, GHC.Classes.Eq a) => GHC.Classes.Eq (Data.Mergeless.SyncRequest i a)
- Data.Mergeless: instance (GHC.Classes.Ord a, GHC.Classes.Ord i) => GHC.Classes.Ord (Data.Mergeless.CentralStore i a)
- Data.Mergeless: instance (GHC.Classes.Ord a, GHC.Classes.Ord i) => GHC.Classes.Ord (Data.Mergeless.Synced i a)
- Data.Mergeless: instance (GHC.Classes.Ord i, GHC.Classes.Ord a) => GHC.Classes.Ord (Data.Mergeless.StoreItem i a)
- Data.Mergeless: instance (GHC.Classes.Ord i, GHC.Classes.Ord a) => GHC.Classes.Ord (Data.Mergeless.SyncRequest i a)
- Data.Mergeless: instance (GHC.Show.Show a, GHC.Show.Show i) => GHC.Show.Show (Data.Mergeless.CentralStore i a)
- Data.Mergeless: instance (GHC.Show.Show a, GHC.Show.Show i) => GHC.Show.Show (Data.Mergeless.Synced i a)
- Data.Mergeless: instance (GHC.Show.Show i, GHC.Show.Show a) => GHC.Show.Show (Data.Mergeless.StoreItem i a)
- Data.Mergeless: instance (GHC.Show.Show i, GHC.Show.Show a) => GHC.Show.Show (Data.Mergeless.SyncRequest i a)
+ Data.Mergeless: SyncProcessor :: Set i -> m () -> Set i -> m (Set i) -> Set i -> m (Set (Synced i a)) -> Set (Synced i a) -> m () -> SyncProcessor i a m
+ Data.Mergeless: [syncProcessorDeleteMany] :: SyncProcessor i a m -> Set i -> m ()
+ Data.Mergeless: [syncProcessorInsertMany] :: SyncProcessor i a m -> Set (Synced i a) -> m ()
+ Data.Mergeless: [syncProcessorQueryNewRemote] :: SyncProcessor i a m -> Set i -> m (Set (Synced i a))
+ Data.Mergeless: [syncProcessorQuerySynced] :: SyncProcessor i a m -> Set i -> m (Set i)
+ Data.Mergeless: addItemToStore :: (Ord i, Ord a) => Added a -> Store i a -> Store i a
+ Data.Mergeless: data SyncProcessor i a m
+ Data.Mergeless: deleteSynced :: (Ord i, Ord a) => Synced i a -> Store i a -> Store i a
+ Data.Mergeless: deleteUnsynced :: (Ord i, Ord a) => Added a -> Store i a -> Store i a
+ Data.Mergeless: emptyStore :: Store i a
+ Data.Mergeless: instance (Data.Aeson.Types.FromJSON.FromJSONKey i, GHC.Classes.Ord i, Data.Aeson.Types.FromJSON.FromJSON a) => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeless.CentralStore i a)
+ Data.Mergeless: instance (Data.Aeson.Types.ToJSON.ToJSON a, Data.Aeson.Types.ToJSON.ToJSONKey i) => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeless.CentralStore i a)
+ Data.Mergeless: instance (Data.Aeson.Types.ToJSON.ToJSON i, Data.Aeson.Types.ToJSON.ToJSON a) => Data.Aeson.Types.ToJSON.ToJSON (Data.Mergeless.Store i a)
+ Data.Mergeless: instance (GHC.Classes.Eq a, GHC.Classes.Eq i) => GHC.Classes.Eq (Data.Mergeless.StoreItem i a)
+ Data.Mergeless: instance (GHC.Classes.Eq a, GHC.Classes.Eq i) => GHC.Classes.Eq (Data.Mergeless.SyncRequest i a)
+ Data.Mergeless: instance (GHC.Classes.Eq i, GHC.Classes.Eq a) => GHC.Classes.Eq (Data.Mergeless.CentralStore i a)
+ Data.Mergeless: instance (GHC.Classes.Eq i, GHC.Classes.Eq a) => GHC.Classes.Eq (Data.Mergeless.Synced i a)
+ Data.Mergeless: instance (GHC.Classes.Ord a, GHC.Classes.Ord i) => GHC.Classes.Ord (Data.Mergeless.StoreItem i a)
+ Data.Mergeless: instance (GHC.Classes.Ord a, GHC.Classes.Ord i) => GHC.Classes.Ord (Data.Mergeless.SyncRequest i a)
+ Data.Mergeless: instance (GHC.Classes.Ord a, GHC.Classes.Ord i, Data.Aeson.Types.FromJSON.FromJSON i, Data.Aeson.Types.FromJSON.FromJSON a) => Data.Aeson.Types.FromJSON.FromJSON (Data.Mergeless.Store i a)
+ Data.Mergeless: instance (GHC.Classes.Ord i, GHC.Classes.Ord a) => GHC.Classes.Ord (Data.Mergeless.CentralStore i a)
+ Data.Mergeless: instance (GHC.Classes.Ord i, GHC.Classes.Ord a) => GHC.Classes.Ord (Data.Mergeless.Synced i a)
+ Data.Mergeless: instance (GHC.Show.Show a, GHC.Show.Show i) => GHC.Show.Show (Data.Mergeless.StoreItem i a)
+ Data.Mergeless: instance (GHC.Show.Show a, GHC.Show.Show i) => GHC.Show.Show (Data.Mergeless.SyncRequest i a)
+ Data.Mergeless: instance (GHC.Show.Show i, GHC.Show.Show a) => GHC.Show.Show (Data.Mergeless.CentralStore i a)
+ Data.Mergeless: instance (GHC.Show.Show i, GHC.Show.Show a) => GHC.Show.Show (Data.Mergeless.Synced i a)
+ Data.Mergeless: instance GHC.Generics.Generic (Data.Mergeless.SyncProcessor i a m)
+ Data.Mergeless: processSyncCustom :: forall i a m. (Ord i, Ord a, Monad m) => m i -> UTCTime -> SyncProcessor i a m -> SyncRequest i a -> m (SyncResponse i a)
+ Data.Mergeless: storeSize :: Store i a -> Int
- Data.Mergeless: UndeletedItem :: i -> StoreItem i a
+ Data.Mergeless: UndeletedItem :: !i -> StoreItem i a

Files

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