diff options
Diffstat (limited to 'src/Erebos/Storage')
| -rw-r--r-- | src/Erebos/Storage/Backend.hs | 28 | ||||
| -rw-r--r-- | src/Erebos/Storage/Disk.hs | 230 | ||||
| -rw-r--r-- | src/Erebos/Storage/Head.hs | 258 | ||||
| -rw-r--r-- | src/Erebos/Storage/Internal.hs | 291 | ||||
| -rw-r--r-- | src/Erebos/Storage/Key.hs | 52 | ||||
| -rw-r--r-- | src/Erebos/Storage/List.hs | 154 | ||||
| -rw-r--r-- | src/Erebos/Storage/Memory.hs | 101 | ||||
| -rw-r--r-- | src/Erebos/Storage/Merge.hs | 164 | 
8 files changed, 1278 insertions, 0 deletions
| diff --git a/src/Erebos/Storage/Backend.hs b/src/Erebos/Storage/Backend.hs new file mode 100644 index 0000000..620d423 --- /dev/null +++ b/src/Erebos/Storage/Backend.hs @@ -0,0 +1,28 @@ +{-| +Description: Implement custom storage backend + +Exports type class, which can be used to create custom 'Storage' backend. +-} + +module Erebos.Storage.Backend ( +    StorageBackend(..), +    Complete, Partial, +    Storage, PartialStorage, +    newStorage, + +    WatchID, startWatchID, nextWatchID, +) where + +import Control.Concurrent.MVar + +import Data.HashTable.IO qualified as HT + +import Erebos.Object.Internal +import Erebos.Storage.Internal + + +newStorage :: StorageBackend bck => bck -> IO (Storage' (BackendCompleteness bck)) +newStorage stBackend = do +    stRefGeneration <- newMVar =<< HT.new +    stRefRoots <- newMVar =<< HT.new +    return Storage {..} diff --git a/src/Erebos/Storage/Disk.hs b/src/Erebos/Storage/Disk.hs new file mode 100644 index 0000000..8e35940 --- /dev/null +++ b/src/Erebos/Storage/Disk.hs @@ -0,0 +1,230 @@ +module Erebos.Storage.Disk ( +    openStorage, +) where + +import Codec.Compression.Zlib + +import Control.Arrow +import Control.Concurrent +import Control.Exception +import Control.Monad + +import Data.ByteArray qualified as BA +import Data.ByteString (ByteString) +import Data.ByteString qualified as B +import Data.ByteString.Char8 qualified as BC +import Data.ByteString.Lazy qualified as BL +import Data.ByteString.Lazy.Char8 qualified as BLC +import Data.Function +import Data.List +import Data.Maybe + +import System.Directory +import System.FSNotify +import System.FilePath +import System.IO +import System.IO.Error + +import Erebos.Object +import Erebos.Storage.Backend +import Erebos.Storage.Head +import Erebos.Storage.Internal +import Erebos.Storage.Platform +import Erebos.UUID qualified as U + + +data DiskStorage = StorageDir +    { dirPath :: FilePath +    , dirWatchers :: MVar ( Maybe WatchManager, [ HeadTypeID ], WatchList ) +    } + +instance Eq DiskStorage where +    (==) = (==) `on` dirPath + +instance Show DiskStorage where +    show StorageDir { dirPath = path } = "dir:" ++ path + +instance StorageBackend DiskStorage where +    backendLoadBytes StorageDir {..} dgst = +        handleJust (guard . isDoesNotExistError) (const $ return Nothing) $ +              Just . decompress . BL.fromChunks . (:[]) <$> (B.readFile $ refPath dirPath dgst) +    backendStoreBytes StorageDir {..} dgst = writeFileOnce (refPath dirPath dgst) . compress + + +    backendLoadHeads StorageDir {..} tid = do +        let hpath = headTypePath dirPath tid + +        files <- filterM (doesFileExist . (hpath </>)) =<< +            handleJust (\e -> guard (isDoesNotExistError e)) (const $ return []) +            (getDirectoryContents hpath) +        fmap catMaybes $ forM files $ \hname -> do +            case U.fromString hname of +                 Just hid -> do +                     content <- B.readFile (hpath </> hname) +                     return $ do +                         (h : _) <- Just (BC.lines content) +                         dgst <- readRefDigest h +                         Just $ ( HeadID hid, dgst ) +                 Nothing -> return Nothing + +    backendLoadHead StorageDir {..} tid hid = do +        handleJust (guard . isDoesNotExistError) (const $ return Nothing) $ do +            (h:_) <- BC.lines <$> B.readFile (headPath dirPath tid hid) +            return $ readRefDigest h + +    backendStoreHead StorageDir {..} tid hid dgst = do +         Right () <- writeFileChecked (headPath dirPath tid hid) Nothing $ +             showRefDigest dgst `B.append` BC.singleton '\n' +         return () + +    backendReplaceHead StorageDir {..} tid hid expected new = do +         let filename = headPath dirPath tid hid +             showDgstL r = showRefDigest r `B.append` BC.singleton '\n' + +         writeFileChecked filename (Just $ showDgstL expected) (showDgstL new) >>= \case +             Left Nothing -> return $ Left Nothing +             Left (Just bs) -> do Just cur <- return $ readRefDigest $ BC.takeWhile (/='\n') bs +                                  return $ Left $ Just cur +             Right () -> return $ Right new + +    backendWatchHead st@StorageDir {..} tid hid cb = do +        modifyMVar dirWatchers $ \( mbmanager, ilist, wl ) -> do +            manager <- maybe startManager return mbmanager +            ilist' <- case tid `elem` ilist of +                True -> return ilist +                False -> do +                    void $ watchDir manager (headTypePath dirPath tid) (const True) $ \case +                        ev@Added {} | Just ihid <- HeadID <$> U.fromString (takeFileName (eventPath ev)) -> do +                            backendLoadHead st tid ihid >>= \case +                                Just dgst -> do +                                    (_, _, iwl) <- readMVar dirWatchers +                                    mapM_ ($ dgst) . map wlFun . filter ((== (tid, ihid)) . wlHead) . wlList $ iwl +                                Nothing -> return () +                        _ -> return () +                    return $ tid : ilist +            return $ first ( Just manager, ilist', ) $ watchListAdd tid hid cb wl + +    backendUnwatchHead StorageDir {..} wid = do +        modifyMVar_ dirWatchers $ \( mbmanager, ilist, wl ) -> do +            return ( mbmanager, ilist, watchListDel wid wl ) + + +    backendListKeys StorageDir {..} = do +        catMaybes . map (readRefDigest . BC.pack) <$> +            listDirectory (keyDirPath dirPath) + +    backendLoadKey StorageDir {..} dgst = do +        tryIOError (BC.readFile (keyFilePath dirPath dgst)) >>= \case +            Right kdata -> return $ Just $ BA.convert kdata +            Left _ -> return Nothing + +    backendStoreKey StorageDir {..} dgst key = do +        writeFileOnce (keyFilePath dirPath dgst) (BL.fromStrict $ BA.convert key) + +    backendRemoveKey StorageDir {..} dgst = do +        void $ tryIOError (removeFile $ keyFilePath dirPath dgst) + + +storageVersion :: String +storageVersion = "0.1" + +openStorage :: FilePath -> IO Storage +openStorage path = modifyIOError annotate $ do +    let versionFileName = "erebos-storage" +    let versionPath = path </> versionFileName +    let writeVersionFile = writeFileOnce versionPath $ BLC.pack $ storageVersion <> "\n" + +    maybeVersion <- handleJust (guard . isDoesNotExistError) (const $ return Nothing) $ +        Just <$> readFile versionPath +    version <- case maybeVersion of +        Just versionContent -> do +            return $ takeWhile (/= '\n') versionContent + +        Nothing -> do +            files <- handleJust (guard . isDoesNotExistError) (const $ return []) $ +                listDirectory path +            when (not $ or +                    [ null files +                    , versionFileName `elem` files +                    , (versionFileName ++ ".lock") `elem` files +                    , "objects" `elem` files && "heads" `elem` files +                    ]) $ do +                fail "directory is neither empty, nor an existing erebos storage" + +            createDirectoryIfMissing True $ path +            writeVersionFile +            takeWhile (/= '\n') <$> readFile versionPath + +    when (version /= storageVersion) $ do +        fail $ "unsupported storage version " <> version + +    createDirectoryIfMissing True $ path </> "objects" +    createDirectoryIfMissing True $ path </> "heads" +    watchers <- newMVar ( Nothing, [], WatchList startWatchID [] ) +    newStorage $ StorageDir path watchers +  where +    annotate e = annotateIOError e "failed to open storage" Nothing (Just path) + + +refPath :: FilePath -> RefDigest -> FilePath +refPath spath rdgst = intercalate "/" [ spath, "objects", BC.unpack alg, pref, rest ] +    where (alg, dgst) = showRefDigestParts rdgst +          (pref, rest) = splitAt 2 $ BC.unpack dgst + +headTypePath :: FilePath -> HeadTypeID -> FilePath +headTypePath spath (HeadTypeID tid) = spath </> "heads" </> U.toString tid + +headPath :: FilePath -> HeadTypeID -> HeadID -> FilePath +headPath spath tid (HeadID hid) = headTypePath spath tid </> U.toString hid + +keyDirPath :: FilePath -> FilePath +keyDirPath sdir = sdir </> "keys" + +keyFilePath :: FilePath -> RefDigest -> FilePath +keyFilePath sdir dgst = keyDirPath sdir </> (BC.unpack $ showRefDigest dgst) + + +openLockFile :: FilePath -> IO Handle +openLockFile path = do +    createDirectoryIfMissing True (takeDirectory path) +    retry 10 $ createFileExclusive path +  where +    retry :: Int -> IO a -> IO a +    retry 0 act = act +    retry n act = catchJust (\e -> if isAlreadyExistsError e then Just () else Nothing) +                      act (\_ -> threadDelay (100 * 1000) >> retry (n - 1) act) + +writeFileOnce :: FilePath -> BL.ByteString -> IO () +writeFileOnce file content = bracket (openLockFile locked) +    hClose $ \h -> do +        doesFileExist file >>= \case +            True  -> removeFile locked +            False -> do BL.hPut h content +                        hClose h +                        renameFile locked file +    where locked = file ++ ".lock" + +writeFileChecked :: FilePath -> Maybe ByteString -> ByteString -> IO (Either (Maybe ByteString) ()) +writeFileChecked file prev content = bracket (openLockFile locked) +    hClose $ \h -> do +        (prev,) <$> doesFileExist file >>= \case +            (Nothing, True) -> do +                current <- B.readFile file +                removeFile locked +                return $ Left $ Just current +            (Nothing, False) -> do B.hPut h content +                                   hClose h +                                   renameFile locked file +                                   return $ Right () +            (Just expected, True) -> do +                current <- B.readFile file +                if current == expected then do B.hPut h content +                                               hClose h +                                               renameFile locked file +                                               return $ return () +                                       else do removeFile locked +                                               return $ Left $ Just current +            (Just _, False) -> do +                removeFile locked +                return $ Left Nothing +    where locked = file ++ ".lock" diff --git a/src/Erebos/Storage/Head.hs b/src/Erebos/Storage/Head.hs new file mode 100644 index 0000000..285902d --- /dev/null +++ b/src/Erebos/Storage/Head.hs @@ -0,0 +1,258 @@ +{-| +Description: Define, use and watch heads + +Provides data types and functions for reading, writing or watching `Head's. +Type class `HeadType' is used to define custom new `Head' types. +-} + +module Erebos.Storage.Head ( +    -- * Head type and accessors +    Head, HeadType(..), +    HeadID, HeadTypeID, mkHeadTypeID, +    headId, headStorage, headRef, headObject, headStoredObject, + +    -- * Loading and storing heads +    loadHeads, loadHead, reloadHead, +    storeHead, replaceHead, updateHead, updateHead_, +    loadHeadRaw, storeHeadRaw, replaceHeadRaw, + +    -- * Watching heads +    WatchedHead, +    watchHead, watchHeadWith, unwatchHead, +    watchHeadRaw, +) where + +import Control.Concurrent +import Control.Monad +import Control.Monad.Reader + +import Data.Bifunctor +import Data.Typeable + +import Erebos.Object +import Erebos.Storable +import Erebos.Storage.Backend +import Erebos.Storage.Internal +import Erebos.UUID qualified as U + + +-- | Represents loaded Erebos storage head, along with the object it pointed to +-- at the time it was loaded. +-- +-- Each possible head type has associated unique ID, represented as +-- `HeadTypeID'. For each type, there can be multiple individual heads in given +-- storage, each also identified by unique ID (`HeadID'). +data Head a = Head HeadID (Stored a) +    deriving (Eq, Show) + +-- | Instances of this class can be used as objects pointed to by heads in +-- Erebos storage. Each such type must be `Storable' and have a unique ID. +-- +-- To create a custom head type, generate a new UUID and assign it to the type using +-- `mkHeadTypeID': +-- +-- > instance HeadType MyType where +-- >     headTypeID _ = mkHeadTypeID "86e8033d-c476-4f81-9b7c-fd36b9144475" +class Storable a => HeadType a where +    headTypeID :: proxy a -> HeadTypeID +    -- ^ Get the ID of the given head type; must be unique for each `HeadType' instance. + +instance MonadIO m => MonadStorage (ReaderT (Head a) m) where +    getStorage = asks $ headStorage + + +-- | Get `HeadID' associated with given `Head'. +headId :: Head a -> HeadID +headId (Head uuid _) = uuid + +-- | Get storage from which the `Head' was loaded. +headStorage :: Head a -> Storage +headStorage = refStorage . headRef + +-- | Get `Ref' of the `Head'\'s associated object. +headRef :: Head a -> Ref +headRef (Head _ sx) = storedRef sx + +-- | Get the object the `Head' pointed to when it was loaded. +headObject :: Head a -> a +headObject (Head _ sx) = fromStored sx + +-- | Get the object the `Head' pointed to when it was loaded as a `Stored' value. +headStoredObject :: Head a -> Stored a +headStoredObject (Head _ sx) = sx + +-- | Create `HeadTypeID' from string representation of UUID. +mkHeadTypeID :: String -> HeadTypeID +mkHeadTypeID = maybe (error "Invalid head type ID") HeadTypeID . U.fromString + + +-- | Load all `Head's of type @a@ from storage. +loadHeads :: forall a m. MonadIO m => HeadType a => Storage -> m [Head a] +loadHeads st@Storage {..} = +    map (uncurry Head . fmap (wrappedLoad . Ref st)) +        <$> liftIO (backendLoadHeads stBackend (headTypeID @a Proxy)) + +-- | Try to load a `Head' of type @a@ from storage. +loadHead +    :: forall a m. (HeadType a, MonadIO m) +    => Storage  -- ^ Storage from which to load the head +    -> HeadID  -- ^ ID of the particular head +    -> m (Maybe (Head a))  -- ^ Head object, or `Nothing' if not found +loadHead st hid = fmap (Head hid . wrappedLoad) <$> loadHeadRaw st (headTypeID @a Proxy) hid + +-- | Try to load `Head' using a raw head and type IDs, getting `Ref' if found. +loadHeadRaw +    :: forall m. MonadIO m +    => Storage  -- ^ Storage from which to load the head +    -> HeadTypeID  -- ^ ID of the head type +    -> HeadID  -- ^ ID of the particular head +    -> m (Maybe Ref)  -- ^ `Ref' pointing to the head object, or `Nothing' if not found +loadHeadRaw st@Storage {..} tid hid = do +    fmap (Ref st) <$> liftIO (backendLoadHead stBackend tid hid) + +-- | Reload the given head from storage, returning `Head' with updated object, +-- or `Nothing' if there is no longer head with the particular ID in storage. +reloadHead :: (HeadType a, MonadIO m) => Head a -> m (Maybe (Head a)) +reloadHead (Head hid val) = loadHead (storedStorage val) hid + +-- | Store a new `Head' of type 'a' in the storage. +storeHead :: forall a m. MonadIO m => HeadType a => Storage -> a -> m (Head a) +storeHead st obj = do +    let tid = headTypeID @a Proxy +    stored <- wrappedStore st obj +    hid <- storeHeadRaw st tid (storedRef stored) +    return $ Head hid stored + +-- | Store a new `Head' in the storage, using the raw `HeadTypeID' and `Ref', +-- the function returns the assigned `HeadID' of the new head. +storeHeadRaw :: forall m. MonadIO m => Storage -> HeadTypeID -> Ref -> m HeadID +storeHeadRaw Storage {..} tid ref = liftIO $ do +    hid <- HeadID <$> U.nextRandom +    backendStoreHead stBackend tid hid (refDigest ref) +    return hid + +-- | Try to replace existing `Head' of type @a@ in the storage. Function fails +-- if the head value in storage changed after being loaded here; for automatic +-- retry see `updateHead'. +replaceHead +    :: forall a m. (HeadType a, MonadIO m) +    => Head a  -- ^ Existing head, associated object is supposed to match the one in storage +    -> Stored a  -- ^ Intended new value +    -> m (Either (Maybe (Head a)) (Head a)) +        -- ^ +        -- [@`Left' `Nothing'@]: +        --     Nothing was stored – the head no longer exists in storage. +        -- [@`Left' (`Just' h)@]: +        --     Nothing was stored – the head value in storage does not match +        --     the first parameter, but is @h@ instead. +        -- [@`Right' h@]: +        --     Head value was updated in storage, the new head is @h@ (which is +        --     the same as first parameter with associated object replaced by +        --     the second parameter). +replaceHead prev@(Head hid pobj) stored' = liftIO $ do +    let st = headStorage prev +        tid = headTypeID @a Proxy +    stored <- copyStored st stored' +    bimap (fmap $ Head hid . wrappedLoad) (const $ Head hid stored) <$> +        replaceHeadRaw st tid hid (storedRef pobj) (storedRef stored) + +-- | Try to replace existing head using raw IDs and `Ref's. +replaceHeadRaw +    :: forall m. MonadIO m +    => Storage  -- ^ Storage to use +    -> HeadTypeID  -- ^ ID of the head type +    -> HeadID  -- ^ ID of the particular head +    -> Ref  -- ^ Expected value in storage +    -> Ref  -- ^ Intended new value +    -> m (Either (Maybe Ref) Ref) +        -- ^ +        -- [@`Left' `Nothing'@]: +        --     Nothing was stored – the head no longer exists in storage. +        -- [@`Left' (`Just' r)@]: +        --     Nothing was stored – the head value in storage does not match +        --     the expected value, but is @r@ instead. +        -- [@`Right' r@]: +        --     Head value was updated in storage, the new head value is @r@ +        --     (which is the same as the indended value). +replaceHeadRaw st@Storage {..} tid hid prev new = liftIO $ do +    _ <- copyRef st new +    bimap (fmap $ Ref st) (Ref st) <$> backendReplaceHead stBackend tid hid (refDigest prev) (refDigest new) + +-- | Update existing existing `Head' of type @a@ in the storage, using a given +-- function. The update function may be called multiple times in case the head +-- content changes concurrently during evaluation. +updateHead +    :: (HeadType a, MonadIO m) +    => Head a  -- ^ Existing head to be updated +    -> (Stored a -> m ( Stored a, b )) +        -- ^ Function that gets current value of the head and returns updated +        -- value, along with a custom extra value to be returned from +        -- `updateHead' call. The function may be called multiple times. +    -> m ( Maybe (Head a), b ) +        -- ^ First element contains either the new head as @`Just' h@, or +        -- `Nothing' in case the head no longer exists in storage. Second +        -- element is the value from last call to the update function. +updateHead h f = do +    (o, x) <- f $ headStoredObject h +    replaceHead h o >>= \case +        Right h' -> return (Just h', x) +        Left Nothing -> return (Nothing, x) +        Left (Just h') -> updateHead h' f + +-- | Update existing existing `Head' of type @a@ in the storage, using a given +-- function. The update function may be called multiple times in case the head +-- content changes concurrently during evaluation. +updateHead_ +    :: (HeadType a, MonadIO m) +    => Head a  -- ^ Existing head to be updated +    -> (Stored a -> m (Stored a)) +        -- ^ Function that gets current value of the head and returns updated +        -- value; may be called multiple times. +    -> m (Maybe (Head a)) +        -- ^ The new head as @`Just' h@, or `Nothing' in case the head no +        -- longer exists in storage. +updateHead_ h = fmap fst . updateHead h . (fmap (,()) .) + + +-- | Represents a handle of a watched head, which can be used to cancel the +-- watching. +data WatchedHead = forall a. WatchedHead Storage WatchID (MVar a) + +-- | Watch the given head. The callback will be called with the current head +-- value, and then again each time the head changes. +watchHead :: forall a. HeadType a => Head a -> (Head a -> IO ()) -> IO WatchedHead +watchHead h = watchHeadWith h id + +-- | Watch the given head using custom selector function. The callback will be +-- called with the value derived from current head state, and then again each +-- time the selected value changes according to its `Eq' instance. +watchHeadWith +    :: forall a b. (HeadType a, Eq b) +    => Head a  -- ^ Head to watch +    -> (Head a -> b)  -- ^ Selector function +    -> (b -> IO ())  -- ^ Callback +    -> IO WatchedHead  -- ^ Watched head handle +watchHeadWith (Head hid val) sel cb = do +    watchHeadRaw (storedStorage val) (headTypeID @a Proxy) hid (sel . Head hid . wrappedLoad) cb + +-- | Watch the given head using raw IDs and a selector from `Ref'. +watchHeadRaw :: forall b. Eq b => Storage -> HeadTypeID -> HeadID -> (Ref -> b) -> (b -> IO ()) -> IO WatchedHead +watchHeadRaw st@Storage {..} tid hid sel cb = do +    memo <- newEmptyMVar +    let cb' dgst = do +            let x = sel (Ref st dgst) +            modifyMVar_ memo $ \prev -> do +                when (Just x /= prev) $ cb x +                return $ Just x +    wid <- backendWatchHead stBackend tid hid cb' + +    cur <- fmap sel <$> loadHeadRaw st tid hid +    maybe (return ()) cb cur +    putMVar memo cur + +    return $ WatchedHead st wid memo + +-- | Stop watching previously watched head. +unwatchHead :: WatchedHead -> IO () +unwatchHead (WatchedHead Storage {..} wid _) = do +    backendUnwatchHead stBackend wid diff --git a/src/Erebos/Storage/Internal.hs b/src/Erebos/Storage/Internal.hs new file mode 100644 index 0000000..db211bb --- /dev/null +++ b/src/Erebos/Storage/Internal.hs @@ -0,0 +1,291 @@ +module Erebos.Storage.Internal ( +    Storage'(..), Storage, PartialStorage, +    Ref'(..), Ref, PartialRef, +    RefDigest(..), +    WatchID, startWatchID, nextWatchID, +    WatchList(..), WatchListItem(..), watchListAdd, watchListDel, + +    refStorage, +    refDigest, refDigestFromByteString, +    showRef, showRefDigest, showRefDigestParts, +    readRefDigest, +    hashToRefDigest, + +    StorageCompleteness(..), +    StorageBackend(..), +    Complete, Partial, + +    unsafeStoreRawBytes, +    ioLoadBytesFromStorage, + +    Generation(..), +    HeadID(..), HeadTypeID(..), +    Stored(..), storedStorage, +) where + +import Control.Arrow +import Control.Concurrent +import Control.DeepSeq +import Control.Exception +import Control.Monad.Identity + +import Crypto.Hash + +import Data.Bits +import Data.ByteArray (ByteArrayAccess, ScrubbedBytes) +import Data.ByteArray qualified as BA +import Data.ByteString (ByteString) +import Data.ByteString.Char8 qualified as BC +import Data.ByteString.Lazy qualified as BL +import Data.Function +import Data.HashTable.IO qualified as HT +import Data.Hashable +import Data.Kind +import Data.Typeable + +import Foreign.Storable (peek) + +import System.IO.Unsafe (unsafePerformIO) + +import Erebos.UUID (UUID) +import Erebos.Util + + +data Storage' c = forall bck. (StorageBackend bck, BackendCompleteness bck ~ c) => Storage +    { stBackend :: bck +    , stRefGeneration :: MVar (HT.BasicHashTable RefDigest Generation) +    , stRefRoots :: MVar (HT.BasicHashTable RefDigest [RefDigest]) +    } + +type Storage = Storage' Complete +type PartialStorage = Storage' Partial + +instance Eq (Storage' c) where +    Storage { stBackend = b } == Storage { stBackend = b' } +        | Just b'' <- cast b' =  b == b'' +        | otherwise           =  False + +instance Show (Storage' c) where +    show Storage { stBackend = b } = show b ++ showParentStorage b + +showParentStorage :: StorageBackend bck => bck -> String +showParentStorage bck +    | Just (st :: Storage) <- cast (backendParent bck) = "@" ++ show st +    | Just (st :: PartialStorage) <- cast (backendParent bck) = "@" ++ show st +    | otherwise = "" + + +class (Eq bck, Show bck, Typeable bck, Typeable (BackendParent bck)) => StorageBackend bck where +    type BackendCompleteness bck :: Type -> Type +    type BackendCompleteness bck = Complete + +    type BackendParent bck :: Type +    type BackendParent bck = () +    backendParent :: bck -> BackendParent bck +    default backendParent :: BackendParent bck ~ () => bck -> BackendParent bck +    backendParent _ = () + + +    backendLoadBytes :: bck -> RefDigest -> IO (Maybe BL.ByteString) +    default backendLoadBytes :: BackendParent bck ~ Storage => bck -> RefDigest -> IO (Maybe BL.ByteString) +    backendLoadBytes bck = case backendParent bck of Storage { stBackend = bck' } -> backendLoadBytes bck' + +    backendStoreBytes :: bck -> RefDigest -> BL.ByteString -> IO () +    default backendStoreBytes :: BackendParent bck ~ Storage => bck -> RefDigest -> BL.ByteString -> IO () +    backendStoreBytes bck = case backendParent bck of Storage { stBackend = bck' } -> backendStoreBytes bck' + + +    backendLoadHeads :: bck -> HeadTypeID -> IO [ ( HeadID, RefDigest ) ] +    default backendLoadHeads :: BackendParent bck ~ Storage => bck -> HeadTypeID -> IO [ ( HeadID, RefDigest ) ] +    backendLoadHeads bck = case backendParent bck of Storage { stBackend = bck' } -> backendLoadHeads bck' + +    backendLoadHead :: bck -> HeadTypeID -> HeadID -> IO (Maybe RefDigest) +    default backendLoadHead :: BackendParent bck ~ Storage => bck -> HeadTypeID -> HeadID -> IO (Maybe RefDigest) +    backendLoadHead bck = case backendParent bck of Storage { stBackend = bck' } -> backendLoadHead bck' + +    backendStoreHead :: bck -> HeadTypeID -> HeadID -> RefDigest -> IO () +    default backendStoreHead :: BackendParent bck ~ Storage => bck -> HeadTypeID -> HeadID -> RefDigest -> IO () +    backendStoreHead bck = case backendParent bck of Storage { stBackend = bck' } -> backendStoreHead bck' + +    backendReplaceHead :: bck -> HeadTypeID -> HeadID -> RefDigest -> RefDigest -> IO (Either (Maybe RefDigest) RefDigest) +    default backendReplaceHead :: BackendParent bck ~ Storage => bck -> HeadTypeID -> HeadID -> RefDigest -> RefDigest -> IO (Either (Maybe RefDigest) RefDigest) +    backendReplaceHead bck = case backendParent bck of Storage { stBackend = bck' } -> backendReplaceHead bck' + +    backendWatchHead :: bck -> HeadTypeID -> HeadID -> (RefDigest -> IO ()) -> IO WatchID +    default backendWatchHead :: BackendParent bck ~ Storage => bck -> HeadTypeID -> HeadID -> (RefDigest -> IO ()) -> IO WatchID +    backendWatchHead bck = case backendParent bck of Storage { stBackend = bck' } -> backendWatchHead bck' + +    backendUnwatchHead :: bck -> WatchID -> IO () +    default backendUnwatchHead :: BackendParent bck ~ Storage => bck -> WatchID -> IO () +    backendUnwatchHead bck = case backendParent bck of Storage { stBackend = bck' } -> backendUnwatchHead bck' + + +    backendListKeys :: bck -> IO [ RefDigest ] +    default backendListKeys :: BackendParent bck ~ Storage => bck -> IO [ RefDigest ] +    backendListKeys bck = case backendParent bck of Storage { stBackend = bck' } -> backendListKeys bck' + +    backendLoadKey :: bck -> RefDigest -> IO (Maybe ScrubbedBytes) +    default backendLoadKey :: BackendParent bck ~ Storage => bck -> RefDigest -> IO (Maybe ScrubbedBytes) +    backendLoadKey bck = case backendParent bck of Storage { stBackend = bck' } -> backendLoadKey bck' + +    backendStoreKey :: bck -> RefDigest -> ScrubbedBytes -> IO () +    default backendStoreKey :: BackendParent bck ~ Storage => bck -> RefDigest -> ScrubbedBytes -> IO () +    backendStoreKey bck = case backendParent bck of Storage { stBackend = bck' } -> backendStoreKey bck' + +    backendRemoveKey :: bck -> RefDigest -> IO () +    default backendRemoveKey :: BackendParent bck ~ Storage => bck -> RefDigest -> IO () +    backendRemoveKey bck = case backendParent bck of Storage { stBackend = bck' } -> backendRemoveKey bck' + + + +newtype WatchID = WatchID Int +    deriving (Eq, Ord) + +startWatchID :: WatchID +startWatchID = WatchID 1 + +nextWatchID :: WatchID -> WatchID +nextWatchID (WatchID n) = WatchID (n + 1) + +data WatchList = WatchList +    { wlNext :: WatchID +    , wlList :: [ WatchListItem ] +    } + +data WatchListItem = WatchListItem +    { wlID :: WatchID +    , wlHead :: ( HeadTypeID, HeadID ) +    , wlFun :: RefDigest -> IO () +    } + +watchListAdd :: HeadTypeID -> HeadID -> (RefDigest -> IO ()) -> WatchList -> ( WatchList, WatchID ) +watchListAdd tid hid cb wl = ( wl', wlNext wl ) +  where +    wl' = wl +        { wlNext = nextWatchID (wlNext wl) +        , wlList = WatchListItem +            { wlID = wlNext wl +            , wlHead = (tid, hid) +            , wlFun = cb +            } : wlList wl +        } + +watchListDel :: WatchID -> WatchList -> WatchList +watchListDel wid wl = wl { wlList = filter ((/= wid) . wlID) $ wlList wl } + + +newtype RefDigest = RefDigest (Digest Blake2b_256) +    deriving (Eq, Ord, NFData, ByteArrayAccess) + +instance Show RefDigest where +    show = BC.unpack . showRefDigest + +data Ref' c = Ref (Storage' c) RefDigest + +type Ref = Ref' Complete +type PartialRef = Ref' Partial + +instance Eq (Ref' c) where +    Ref _ d1 == Ref _ d2  =  d1 == d2 + +instance Show (Ref' c) where +    show ref@(Ref st _) = show st ++ ":" ++ BC.unpack (showRef ref) + +instance ByteArrayAccess (Ref' c) where +    length (Ref _ dgst) = BA.length dgst +    withByteArray (Ref _ dgst) = BA.withByteArray dgst + +instance Hashable RefDigest where +    hashWithSalt salt ref = salt `xor` unsafePerformIO (BA.withByteArray ref peek) + +instance Hashable (Ref' c) where +    hashWithSalt salt ref = salt `xor` unsafePerformIO (BA.withByteArray ref peek) + +refStorage :: Ref' c -> Storage' c +refStorage (Ref st _) = st + +refDigest :: Ref' c -> RefDigest +refDigest (Ref _ dgst) = dgst + +showRef :: Ref' c -> ByteString +showRef = showRefDigest . refDigest + +showRefDigestParts :: RefDigest -> (ByteString, ByteString) +showRefDigestParts x = (BC.pack "blake2", showHex x) + +showRefDigest :: RefDigest -> ByteString +showRefDigest = showRefDigestParts >>> \(alg, hex) -> alg <> BC.pack "#" <> hex + +readRefDigest :: ByteString -> Maybe RefDigest +readRefDigest x = case BC.split '#' x of +                       [alg, dgst] | BA.convert alg == BC.pack "blake2" -> +                           refDigestFromByteString =<< readHex dgst +                       _ -> Nothing + +refDigestFromByteString :: ByteString -> Maybe RefDigest +refDigestFromByteString = fmap RefDigest . digestFromByteString + +hashToRefDigest :: BL.ByteString -> RefDigest +hashToRefDigest = RefDigest . hashFinalize . hashUpdates hashInit . BL.toChunks + + +newtype Generation = Generation Int +    deriving (Eq, Show) + +-- | UUID of individual Erebos storage head. +newtype HeadID = HeadID UUID +    deriving (Eq, Ord, Show) + +-- | UUID of Erebos storage head type. +newtype HeadTypeID = HeadTypeID UUID +    deriving (Eq, Ord) + +data Stored a = Stored +    { storedRef' :: Ref +    , storedObject' :: a +    } +    deriving (Show) + +instance Eq (Stored a) where +    (==)  =  (==) `on` (refDigest . storedRef') + +instance Ord (Stored a) where +    compare  =  compare `on` (refDigest . storedRef') + +storedStorage :: Stored a -> Storage +storedStorage = refStorage . storedRef' + + +type Complete = Identity +type Partial = Either RefDigest + +class (Traversable compl, Monad compl, Typeable compl) => StorageCompleteness compl where +    type LoadResult compl a :: Type +    returnLoadResult :: compl a -> LoadResult compl a +    ioLoadBytes :: Ref' compl -> IO (compl BL.ByteString) + +instance StorageCompleteness Complete where +    type LoadResult Complete a = a +    returnLoadResult = runIdentity +    ioLoadBytes ref@(Ref st dgst) = maybe (error $ "Ref not found in complete storage: "++show ref) Identity +        <$> ioLoadBytesFromStorage st dgst + +instance StorageCompleteness Partial where +    type LoadResult Partial a = Either RefDigest a +    returnLoadResult = id +    ioLoadBytes (Ref st dgst) = maybe (Left dgst) Right <$> ioLoadBytesFromStorage st dgst + +unsafeStoreRawBytes :: Storage' c -> BL.ByteString -> IO (Ref' c) +unsafeStoreRawBytes st@Storage {..} raw = do +    dgst <- evaluate $ force $ hashToRefDigest raw +    backendStoreBytes stBackend dgst raw +    return $ Ref st dgst + +ioLoadBytesFromStorage :: Storage' c -> RefDigest -> IO (Maybe BL.ByteString) +ioLoadBytesFromStorage Storage {..} dgst = +    backendLoadBytes stBackend dgst >>= \case +        Just bytes -> return $ Just bytes +        Nothing +            | Just (parent :: Storage) <- cast (backendParent stBackend) -> ioLoadBytesFromStorage parent dgst +            | Just (parent :: PartialStorage) <- cast (backendParent stBackend) -> ioLoadBytesFromStorage parent dgst +            | otherwise -> return Nothing diff --git a/src/Erebos/Storage/Key.hs b/src/Erebos/Storage/Key.hs new file mode 100644 index 0000000..b615f16 --- /dev/null +++ b/src/Erebos/Storage/Key.hs @@ -0,0 +1,52 @@ +module Erebos.Storage.Key ( +    KeyPair(..), +    storeKey, loadKey, loadKeyMb, +    moveKeys, +) where + +import Control.Monad +import Control.Monad.Except +import Control.Monad.IO.Class + +import Data.ByteArray +import Data.Typeable + +import Erebos.Storable +import Erebos.Storage.Internal + +class Storable pub => KeyPair sec pub | sec -> pub, pub -> sec where +    generateKeys :: Storage -> IO (sec, Stored pub) +    keyGetPublic :: sec -> Stored pub +    keyGetData :: sec -> ScrubbedBytes +    keyFromData :: ScrubbedBytes -> Stored pub -> Maybe sec + + +storeKey :: KeyPair sec pub => sec -> IO () +storeKey key = do +    let spub = keyGetPublic key +    case storedStorage spub of +        Storage {..} -> backendStoreKey stBackend (refDigest $ storedRef spub) (keyGetData key) + +loadKey :: (KeyPair sec pub, MonadIO m, MonadError e m, FromErebosError e) => Stored pub -> m sec +loadKey pub = maybe (throwOtherError $ "secret key not found for " <> show (storedRef pub)) return =<< loadKeyMb pub + +loadKeyMb :: forall sec pub m. (KeyPair sec pub, MonadIO m) => Stored pub -> m (Maybe sec) +loadKeyMb spub = liftIO $ run $ storedStorage spub +  where +    run :: Storage' c -> IO (Maybe sec) +    run Storage {..} = backendLoadKey stBackend (refDigest $ storedRef spub) >>= \case +        Just bytes -> return $ keyFromData bytes spub +        Nothing +            | Just (parent :: Storage) <- cast (backendParent stBackend) -> run parent +            | Just (parent :: PartialStorage) <- cast (backendParent stBackend) -> run parent +            | otherwise -> return Nothing + +moveKeys :: MonadIO m => Storage -> Storage -> m () +moveKeys Storage { stBackend = from } Storage { stBackend = to } = liftIO $ do +    keys <- backendListKeys from +    forM_ keys $ \key -> do +        backendLoadKey from key >>= \case +            Just sec -> do +                backendStoreKey to key sec +                backendRemoveKey from key +            Nothing -> return () diff --git a/src/Erebos/Storage/List.hs b/src/Erebos/Storage/List.hs new file mode 100644 index 0000000..f0f8786 --- /dev/null +++ b/src/Erebos/Storage/List.hs @@ -0,0 +1,154 @@ +module Erebos.Storage.List ( +    StoredList, +    emptySList, fromSList, storedFromSList, +    slistAdd, slistAddS, +    -- TODO slistInsert, slistInsertS, +    slistRemove, slistReplace, slistReplaceS, +    -- TODO mapFromSList, updateOld, + +    -- TODO StoreUpdate(..), +    -- TODO withStoredListItem, withStoredListItemS, +) where + +import Data.List +import Data.Maybe +import qualified Data.Set as S + +import Erebos.Storage +import Erebos.Storage.Internal +import Erebos.Storage.Merge + +data List a = ListNil +            | ListItem { listPrev :: [StoredList a] +                       , listItem :: Maybe (Stored a) +                       , listRemove :: Maybe (Stored (List a)) +                       } + +type StoredList a = Stored (List a) + +instance Storable a => Storable (List a) where +    store' ListNil = storeZero +    store' x@ListItem {} = storeRec $ do +        mapM_ (storeRef "PREV") $ listPrev x +        mapM_ (storeRef "item") $ listItem x +        mapM_ (storeRef "remove") $ listRemove x + +    load' = loadCurrentObject >>= \case +        ZeroObject -> return ListNil +        _ -> loadRec $ ListItem <$> loadRefs "PREV" +                                <*> loadMbRef "item" +                                <*> loadMbRef "remove" + +instance Storable a => ZeroStorable (List a) where +    fromZero _ = ListNil + + +emptySList :: Storable a => Storage -> IO (StoredList a) +emptySList st = wrappedStore st ListNil + +groupsFromSLists :: forall a. Storable a => StoredList a -> [[Stored a]] +groupsFromSLists = helperSelect S.empty . (:[]) +  where +    helperSelect :: S.Set (StoredList a) -> [StoredList a] -> [[Stored a]] +    helperSelect rs xxs | x:xs <- sort $ filterRemoved rs xxs = helper rs x xs +                        | otherwise = [] + +    helper :: S.Set (StoredList a) -> StoredList a -> [StoredList a] -> [[Stored a]] +    helper rs x xs +        | ListNil <- fromStored x +        = [] + +        | Just rm <- listRemove (fromStored x) +        , ans <- ancestors [x] +        , (other, collision) <- partition (S.null . S.intersection ans . ancestors . (:[])) xs +        , cont <- helperSelect (rs `S.union` ancestors [rm]) $ concatMap (listPrev . fromStored) (x : collision) ++ other +        = case catMaybes $ map (listItem . fromStored) (x : collision) of +               [] -> cont +               xis -> xis : cont + +        | otherwise = case listItem (fromStored x) of +                           Nothing -> helperSelect rs $ listPrev (fromStored x) ++ xs +                           Just xi -> [xi] : (helperSelect rs $ listPrev (fromStored x) ++ xs) + +    filterRemoved :: S.Set (StoredList a) -> [StoredList a] -> [StoredList a] +    filterRemoved rs = filter (S.null . S.intersection rs . ancestors . (:[])) + +fromSList :: Mergeable a => StoredList (Component a) -> [a] +fromSList = map merge . groupsFromSLists + +storedFromSList :: (Mergeable a, Storable a) => StoredList (Component a) -> IO [Stored a] +storedFromSList = mapM storeMerge . groupsFromSLists + +slistAdd :: Storable a => a -> StoredList a -> IO (StoredList a) +slistAdd x prev@(Stored (Ref st _) _) = do +    sx <- wrappedStore st x +    slistAddS sx prev + +slistAddS :: Storable a => Stored a -> StoredList a -> IO (StoredList a) +slistAddS sx prev@(Stored (Ref st _) _) = wrappedStore st (ListItem [prev] (Just sx) Nothing) + +{- TODO +slistInsert :: Storable a => Stored a -> a -> StoredList a -> IO (StoredList a) +slistInsert after x prev@(Stored (Ref st _) _) = do +    sx <- wrappedStore st x +    slistInsertS after sx prev + +slistInsertS :: Storable a => Stored a -> Stored a -> StoredList a -> IO (StoredList a) +slistInsertS after sx prev@(Stored (Ref st _) _) = wrappedStore st $ ListItem Nothing (findSListRef after prev) (Just sx) prev +-} + +slistRemove :: Storable a => Stored a -> StoredList a -> IO (StoredList a) +slistRemove rm prev@(Stored (Ref st _) _) = wrappedStore st $ ListItem [prev] Nothing (findSListRef rm prev) + +slistReplace :: Storable a => Stored a -> a -> StoredList a -> IO (StoredList a) +slistReplace rm x prev@(Stored (Ref st _) _) = do +    sx <- wrappedStore st x +    slistReplaceS rm sx prev + +slistReplaceS :: Storable a => Stored a -> Stored a -> StoredList a -> IO (StoredList a) +slistReplaceS rm sx prev@(Stored (Ref st _) _) = wrappedStore st $ ListItem [prev] (Just sx) (findSListRef rm prev) + +findSListRef :: Stored a -> StoredList a -> Maybe (StoredList a) +findSListRef _ (Stored _ ListNil) = Nothing +findSListRef x cur | listItem (fromStored cur) == Just x = Just cur +                   | otherwise                           = listToMaybe $ catMaybes $ map (findSListRef x) $ listPrev $ fromStored cur + +{- TODO +mapFromSList :: Storable a => StoredList a -> Map RefDigest (Stored a) +mapFromSList list = helper list M.empty +    where helper :: Storable a => StoredList a -> Map RefDigest (Stored a) -> Map RefDigest (Stored a) +          helper (Stored _ ListNil) cur = cur +          helper (Stored _ (ListItem (Just rref) _ (Just x) rest)) cur = +              let rxref = case load rref of +                               ListItem _ _ (Just rx) _  -> sameType rx x $ storedRef rx +                               _ -> error "mapFromSList: malformed list" +               in helper rest $ case M.lookup (refDigest $ storedRef x) cur of +                                     Nothing -> M.insert (refDigest rxref) x cur +                                     Just x' -> M.insert (refDigest rxref) x' cur +          helper (Stored _ (ListItem _ _ _ rest)) cur = helper rest cur +          sameType :: a -> a -> b -> b +          sameType _ _ x = x + +updateOld :: Map RefDigest (Stored a) -> Stored a -> Stored a +updateOld m x = fromMaybe x $ M.lookup (refDigest $ storedRef x) m + + +data StoreUpdate a = StoreKeep +                   | StoreReplace a +                   | StoreRemove + +withStoredListItem :: (Storable a) => (a -> Bool) -> StoredList a -> (a -> IO (StoreUpdate a)) -> IO (StoredList a) +withStoredListItem p list f = withStoredListItemS (p . fromStored) list (suMap (wrappedStore $ storedStorage list) <=< f . fromStored) +    where suMap :: Monad m => (a -> m b) -> StoreUpdate a -> m (StoreUpdate b) +          suMap _ StoreKeep = return StoreKeep +          suMap g (StoreReplace x) = return . StoreReplace =<< g x +          suMap _ StoreRemove = return StoreRemove + +withStoredListItemS :: (Storable a) => (Stored a -> Bool) -> StoredList a -> (Stored a -> IO (StoreUpdate (Stored a))) -> IO (StoredList a) +withStoredListItemS p list f = do +    case find p $ storedFromSList list of +         Just sx -> f sx >>= \case StoreKeep -> return list +                                   StoreReplace nx -> slistReplaceS sx nx list +                                   StoreRemove -> slistRemove sx list +         Nothing -> return list +-} diff --git a/src/Erebos/Storage/Memory.hs b/src/Erebos/Storage/Memory.hs new file mode 100644 index 0000000..677e8c5 --- /dev/null +++ b/src/Erebos/Storage/Memory.hs @@ -0,0 +1,101 @@ +module Erebos.Storage.Memory ( +    memoryStorage, +    deriveEphemeralStorage, +    derivePartialStorage, +) where + +import Control.Concurrent.MVar + +import Data.ByteArray (ScrubbedBytes) +import Data.ByteString.Lazy qualified as BL +import Data.Function +import Data.Kind +import Data.List +import Data.Map (Map) +import Data.Map qualified as M +import Data.Maybe +import Data.Typeable + +import Erebos.Object +import Erebos.Storage.Backend +import Erebos.Storage.Head +import Erebos.Storage.Internal + + +data MemoryStorage p (c :: Type -> Type) = StorageMemory +    { memParent :: p +    , memHeads :: MVar [ (( HeadTypeID, HeadID ), RefDigest ) ] +    , memObjs :: MVar (Map RefDigest BL.ByteString) +    , memKeys :: MVar (Map RefDigest ScrubbedBytes) +    , memWatchers :: MVar WatchList +    } + +instance Eq (MemoryStorage p c) where +    (==) = (==) `on` memObjs + +instance Show (MemoryStorage p c) where +    show StorageMemory {} = "mem" + +instance (StorageCompleteness c, Typeable p) => StorageBackend (MemoryStorage p c) where +    type BackendCompleteness (MemoryStorage p c) = c +    type BackendParent (MemoryStorage p c) = p +    backendParent = memParent + +    backendLoadBytes StorageMemory {..} dgst = +        M.lookup dgst <$> readMVar memObjs + +    backendStoreBytes StorageMemory {..} dgst raw = +        modifyMVar_ memObjs (return . M.insert dgst raw) + + +    backendLoadHeads StorageMemory {..} tid = do +        let toRes ( ( tid', hid ), dgst ) +                | tid' == tid = Just ( hid, dgst ) +                | otherwise   = Nothing +        catMaybes . map toRes <$> readMVar memHeads + +    backendLoadHead StorageMemory {..} tid hid = +        lookup (tid, hid) <$> readMVar memHeads + +    backendStoreHead StorageMemory {..} tid hid dgst = +        modifyMVar_ memHeads $ return . (( ( tid, hid ), dgst ) :) + +    backendReplaceHead StorageMemory {..} tid hid expected new = do +        res <- modifyMVar memHeads $ \hs -> do +            ws <- map wlFun . filter ((==(tid, hid)) . wlHead) . wlList <$> readMVar memWatchers +            return $ case partition ((==(tid, hid)) . fst) hs of +                ( [] , _ ) -> ( hs, Left Nothing ) +                (( _, dgst ) : _, hs' ) +                    | dgst == expected -> ((( tid, hid ), new ) : hs', Right ( new, ws )) +                    | otherwise -> ( hs, Left $ Just dgst ) +        case res of +            Right ( dgst, ws ) -> mapM_ ($ dgst) ws >> return (Right dgst) +            Left x -> return $ Left x + +    backendWatchHead StorageMemory {..} tid hid cb = modifyMVar memWatchers $ return . watchListAdd tid hid cb + +    backendUnwatchHead StorageMemory {..} wid = modifyMVar_ memWatchers $ return . watchListDel wid + + +    backendListKeys StorageMemory {..} = M.keys <$> readMVar memKeys +    backendLoadKey StorageMemory {..} dgst = M.lookup dgst <$> readMVar memKeys +    backendStoreKey StorageMemory {..} dgst key = modifyMVar_ memKeys $ return . M.insert dgst key +    backendRemoveKey StorageMemory {..} dgst = modifyMVar_ memKeys $ return . M.delete dgst + + +memoryStorage' :: (StorageCompleteness c, Typeable p) => p -> IO (Storage' c) +memoryStorage' memParent = do +    memHeads <- newMVar [] +    memObjs <- newMVar M.empty +    memKeys <- newMVar M.empty +    memWatchers <- newMVar (WatchList startWatchID []) +    newStorage $ StorageMemory {..} + +memoryStorage :: IO Storage +memoryStorage = memoryStorage' () + +deriveEphemeralStorage :: Storage -> IO Storage +deriveEphemeralStorage parent = memoryStorage' parent + +derivePartialStorage :: Storage -> IO PartialStorage +derivePartialStorage parent = memoryStorage' parent diff --git a/src/Erebos/Storage/Merge.hs b/src/Erebos/Storage/Merge.hs new file mode 100644 index 0000000..a41a65f --- /dev/null +++ b/src/Erebos/Storage/Merge.hs @@ -0,0 +1,164 @@ +module Erebos.Storage.Merge ( +    Mergeable(..), +    merge, storeMerge, + +    Generation, +    showGeneration, +    compareGeneration, generationMax, +    storedGeneration, + +    generations, +    ancestors, +    precedes, +    precedesOrEquals, +    filterAncestors, +    storedRoots, +    walkAncestors, + +    findProperty, +    findPropertyFirst, +) where + +import Control.Concurrent.MVar + +import Data.ByteString.Char8 qualified as BC +import Data.HashTable.IO qualified as HT +import Data.Kind +import Data.List +import Data.Maybe +import Data.Set (Set) +import Data.Set qualified as S + +import System.IO.Unsafe (unsafePerformIO) + +import Erebos.Object +import Erebos.Storable +import Erebos.Storage.Internal +import Erebos.Util + +class Storable (Component a) => Mergeable a where +    type Component a :: Type +    mergeSorted :: [Stored (Component a)] -> a +    toComponents :: a -> [Stored (Component a)] + +instance Mergeable [Stored Object] where +    type Component [Stored Object] = Object +    mergeSorted = id +    toComponents = id + +merge :: Mergeable a => [Stored (Component a)] -> a +merge [] = error "merge: empty list" +merge xs = mergeSorted $ filterAncestors xs + +storeMerge :: (Mergeable a, Storable a) => [Stored (Component a)] -> IO (Stored a) +storeMerge [] = error "merge: empty list" +storeMerge xs@(x : _) = wrappedStore (storedStorage x) $ mergeSorted $ filterAncestors xs + +previous :: Storable a => Stored a -> [Stored a] +previous (Stored ref _) = case load ref of +    Rec items | Just (RecRef dref) <- lookup (BC.pack "SDATA") items +              , Rec ditems <- load dref -> +                    map wrappedLoad $ catMaybes $ map (\case RecRef r -> Just r; _ -> Nothing) $ +                        map snd $ filter ((`elem` [ BC.pack "SPREV", BC.pack "SBASE" ]) . fst) ditems + +              | otherwise -> +                    map wrappedLoad $ catMaybes $ map (\case RecRef r -> Just r; _ -> Nothing) $ +                        map snd $ filter ((`elem` [ BC.pack "PREV", BC.pack "BASE" ]) . fst) items +    _ -> [] + + +nextGeneration :: [Generation] -> Generation +nextGeneration = foldl' helper (Generation 0) +    where helper (Generation c) (Generation n) | c <= n    = Generation (n + 1) +                                               | otherwise = Generation c + +showGeneration :: Generation -> String +showGeneration (Generation x) = show x + +compareGeneration :: Generation -> Generation -> Maybe Ordering +compareGeneration (Generation x) (Generation y) = Just $ compare x y + +generationMax :: Storable a => [Stored a] -> Maybe (Stored a) +generationMax (x : xs) = Just $ snd $ foldl' helper (storedGeneration x, x) xs +    where helper (mg, mx) y = let yg = storedGeneration y +                               in case compareGeneration mg yg of +                                       Just LT -> (yg, y) +                                       _       -> (mg, mx) +generationMax [] = Nothing + +storedGeneration :: Storable a => Stored a -> Generation +storedGeneration x = +    unsafePerformIO $ withMVar (stRefGeneration $ refStorage $ storedRef x) $ \ht -> do +        let doLookup y = HT.lookup ht (refDigest $ storedRef y) >>= \case +                Just gen -> return gen +                Nothing -> do +                    gen <- nextGeneration <$> mapM doLookup (previous y) +                    HT.insert ht (refDigest $ storedRef y) gen +                    return gen +        doLookup x + + +-- |Returns list of sets starting with the set of given objects and +-- intcrementally adding parents. +generations :: Storable a => [Stored a] -> [Set (Stored a)] +generations = unfoldr gen . (,S.empty) +    where gen (hs, cur) = case filter (`S.notMember` cur) hs of +              []    -> Nothing +              added -> let next = foldr S.insert cur added +                        in Just (next, (previous =<< added, next)) + +-- |Returns set containing all given objects and their ancestors +ancestors :: Storable a => [Stored a] -> Set (Stored a) +ancestors = last . (S.empty:) . generations + +precedes :: Storable a => Stored a -> Stored a -> Bool +precedes x y = not $ x `elem` filterAncestors [x, y] + +precedesOrEquals :: Storable a => Stored a -> Stored a -> Bool +precedesOrEquals x y = filterAncestors [ x, y ] == [ y ] + +filterAncestors :: Storable a => [Stored a] -> [Stored a] +filterAncestors [x] = [x] +filterAncestors xs = let xs' = uniq $ sort xs +                      in helper xs' xs' +    where helper remains walk = case generationMax walk of +                                     Just x -> let px = previous x +                                                   remains' = filter (\r -> all (/=r) px) remains +                                                in helper remains' $ uniq $ sort (px ++ filter (/=x) walk) +                                     Nothing -> remains + +storedRoots :: Storable a => Stored a -> [Stored a] +storedRoots x = do +    let st = refStorage $ storedRef x +    unsafePerformIO $ withMVar (stRefRoots st) $ \ht -> do +        let doLookup y = HT.lookup ht (refDigest $ storedRef y) >>= \case +                Just roots -> return roots +                Nothing -> do +                    roots <- case previous y of +                        [] -> return [refDigest $ storedRef y] +                        ps -> map (refDigest . storedRef) . filterAncestors . map (wrappedLoad @Object . Ref st) . concat <$> mapM doLookup ps +                    HT.insert ht (refDigest $ storedRef y) roots +                    return roots +        map (wrappedLoad . Ref st) <$> doLookup x + +walkAncestors :: (Storable a, Monoid m) => (Stored a -> m) -> [Stored a] -> m +walkAncestors f = helper . sortBy cmp +  where +    helper (x : y : xs) | x == y = helper (x : xs) +    helper (x : xs) = f x <> helper (mergeBy cmp (sortBy cmp (previous x)) xs) +    helper [] = mempty + +    cmp x y = case compareGeneration (storedGeneration x) (storedGeneration y) of +                   Just LT -> GT +                   Just GT -> LT +                   _ -> compare x y + +findProperty :: forall a b. Storable a => (a -> Maybe b) -> [Stored a] -> [b] +findProperty sel = map (fromJust . sel . fromStored) . filterAncestors . (findPropHeads sel =<<) + +findPropertyFirst :: forall a b. Storable a => (a -> Maybe b) -> [Stored a] -> Maybe b +findPropertyFirst sel = fmap (fromJust . sel . fromStored) . listToMaybe . filterAncestors . (findPropHeads sel =<<) + +findPropHeads :: forall a b. Storable a => (a -> Maybe b) -> Stored a -> [Stored a] +findPropHeads sel sobj | Just _ <- sel $ fromStored sobj = [sobj] +                       | otherwise = findPropHeads sel =<< previous sobj |