2 Module : Gargantext.Core.NodeStory
3 Description : Node API generation
4 Copyright : (c) CNRS, 2017-Present
5 License : AGPL + CECILL v3
6 Maintainer : team@gargantext.org
7 Stability : experimental
12 {-# OPTIONS_GHC -fno-warn-orphans #-}
13 {-# LANGUAGE TemplateHaskell #-}
14 {-# LANGUAGE ConstraintKinds #-}
16 module Gargantext.Core.NodeStory where
18 import Codec.Serialise (Serialise(), serialise, deserialise)
19 import Control.Concurrent (MVar(), withMVar, newMVar)
20 import Control.Debounce (mkDebounce, defaultDebounceSettings, debounceFreq, debounceAction)
21 import Control.Lens (makeLenses, Getter, (^.))
22 import Control.Monad.Except
23 import Control.Monad.Reader
24 import Data.Aeson hiding ((.=))
25 import Data.Map.Strict (Map)
26 import Data.Maybe (fromMaybe)
29 import GHC.Generics (Generic)
30 import Gargantext.API.Ngrams.Types
31 import Gargantext.Core.Types (NodeId)
32 import Gargantext.Core.Utils.Prefix (unPrefix)
33 import Gargantext.Database.Prelude (CmdM', HasConnectionPool, HasConfig)
34 import Gargantext.Database.Query.Table.Node.Error (HasNodeError())
35 import Gargantext.Prelude
36 import System.Directory (renameFile, createDirectoryIfMissing, doesFileExist)
37 import System.IO (FilePath, hClose)
38 import System.IO.Temp (withTempFile)
39 import qualified Data.ByteString.Lazy as L
40 import qualified Data.List as List
41 import qualified Data.Map.Strict as Map
42 import qualified Data.Map.Strict.Patch.Internal as Patch
43 import qualified Gargantext.Database.Query.Table.Ngrams as TableNgrams
45 ------------------------------------------------------------------------
46 data NodeStoryEnv = NodeStoryEnv
47 { _nse_var :: !(MVar NodeListStory)
48 , _nse_saver :: !(IO ())
49 , _nse_getter :: NodeId -> IO (MVar NodeListStory)
50 --, _nse_cleaner :: !(IO ()) -- every 12 hours: cleans the repos of unused NodeStories
51 -- , _nse_lock :: !FileLock -- TODO (it depends on the option: if with database or file only)
55 type HasNodeStory env err m = ( CmdM' env err m
60 , HasConnectionPool env
64 class (HasNodeStoryVar env, HasNodeStorySaver env)
65 => HasNodeStoryEnv env where
66 hasNodeStory :: Getter env NodeStoryEnv
68 class HasNodeStoryVar env where
69 hasNodeStoryVar :: Getter env (NodeId -> IO (MVar NodeListStory))
71 class HasNodeStorySaver env where
72 hasNodeStorySaver :: Getter env (IO ())
74 ------------------------------------------------------------------------
75 readNodeStoryEnv :: NodeStoryDir -> IO NodeStoryEnv
76 readNodeStoryEnv nsd = do
77 mvar <- nodeStoryVar nsd Nothing 0
78 saver <- mkNodeStorySaver nsd mvar
79 pure $ NodeStoryEnv mvar saver (nodeStoryVar nsd (Just mvar))
81 ------------------------------------------------------------------------
82 mkNodeStorySaver :: NodeStoryDir -> MVar NodeListStory -> IO (IO ())
83 mkNodeStorySaver nsd mvns = mkDebounce settings
85 settings = defaultDebounceSettings
86 { debounceAction = withMVar mvns (writeNodeStories nsd)
87 , debounceFreq = 10 * minute
88 -- , debounceEdge = trailingEdge -- Trigger on the trailing edge
91 second = 10^(6 :: Int)
93 nodeStoryVar :: NodeStoryDir
94 -> Maybe (MVar NodeListStory)
96 -> IO (MVar NodeListStory)
97 nodeStoryVar nsd Nothing ni = nodeStoryInc nsd Nothing ni >>= newMVar
98 nodeStoryVar nsd (Just mv) ni = do
99 mv' <- withMVar mv pure
100 nodeStoryInc nsd (Just mv') ni >>= newMVar
103 nodeStoryInc :: NodeStoryDir -> Maybe NodeListStory -> NodeId -> IO NodeListStory
104 nodeStoryInc nsd (Just ns@(NodeStory nls)) ni = do
105 case Map.lookup ni nls of
107 (NodeStory nls') <- nodeStoryRead nsd ni
108 pure $ NodeStory $ Map.union nls nls'
110 nodeStoryInc nsd Nothing ni = nodeStoryRead nsd ni
114 nodeStoryRead :: NodeStoryDir -> NodeId -> IO NodeListStory
115 nodeStoryRead nsd ni = do
116 _repoDir <- createDirectoryIfMissing True nsd
117 let nsp = nodeStoryPath nsd ni
118 exists <- doesFileExist nsp
120 then deserialise <$> L.readFile nsp
121 else pure (initNodeStory ni)
123 ------------------------------------------------------------------------
124 type NodeStoryDir = FilePath
126 writeNodeStories :: NodeStoryDir -> NodeListStory -> IO ()
127 writeNodeStories fp nls = do
128 _ <- mapM (writeNodeStory fp) $ splitByNode nls
131 writeNodeStory :: NodeStoryDir -> (NodeId, NodeListStory) -> IO ()
132 writeNodeStory rdfp (n, ns) = saverAction' rdfp n ns
134 splitByNode :: NodeListStory -> [(NodeId, NodeListStory)]
135 splitByNode (NodeStory m) =
136 List.map (\(n,a) -> (n, NodeStory $ Map.singleton n a)) $ Map.toList m
139 saverAction' :: NodeStoryDir -> NodeId -> Serialise a => a -> IO ()
140 saverAction' repoDir nId a = do
141 withTempFile repoDir ((cs $ show nId) <> "-tmp-repo.cbor") $ \fp h -> do
142 printDebug "repoSaverAction" fp
143 L.hPut h $ serialise a
145 renameFile fp (nodeStoryPath repoDir nId)
147 nodeStoryPath :: NodeStoryDir -> NodeId -> FilePath
148 nodeStoryPath repoDir nId = repoDir <> "/" <> filename
150 filename = "repo" <> "-" <> (cs $ show nId) <> ".cbor"
154 ------------------------------------------------------------------------
155 -- TODO : repo Migration TODO TESTS
156 repoMigration :: NodeStoryDir -> NgramsRepo -> IO ()
157 repoMigration fp r = writeNodeStories fp (repoToNodeListStory r)
159 repoToNodeListStory :: NgramsRepo -> NodeListStory
160 repoToNodeListStory (Repo _v s h) = NodeStory $ Map.fromList ns
162 s' = ngramsState_migration s
163 h' = ngramsStatePatch_migration h
164 ns = List.map (\(n,ns')
165 -> (n, let hs = fromMaybe [] (Map.lookup n h') in
166 Archive (List.length hs) ns' hs
170 ngramsState_migration :: NgramsState
171 -> [(NodeId,NgramsState')]
172 ngramsState_migration ns =
173 [ (nid, Map.singleton nt table)
174 | (nt, nTable) <- Map.toList ns
175 , (nid, table) <- Map.toList nTable
178 ngramsStatePatch_migration :: [NgramsStatePatch]
179 -> Map NodeId [NgramsStatePatch']
180 ngramsStatePatch_migration np' = Map.fromListWith (<>)
181 [ (nid, [fst $ Patch.singleton nt table])
183 , (nt, nTable) <- Patch.toList np
184 , (nid, table) <- Patch.toList nTable
187 ------------------------------------------------------------------------
189 {- | Node Story for each NodeType where the Key of the Map is NodeId
190 TODO : generalize for any NodeType, let's start with NodeList which
191 is implemented already
193 data NodeStory s p = NodeStory { _unNodeStory :: Map NodeId (Archive s p) }
194 deriving (Generic, Show)
196 instance (FromJSON s, FromJSON p) => FromJSON (NodeStory s p)
197 instance (ToJSON s, ToJSON p) => ToJSON (NodeStory s p)
198 instance (Serialise s, Serialise p) => Serialise (NodeStory s p)
200 data Archive s p = Archive
201 { _a_version :: !Version
204 -- first patch in the list is the most recent
206 deriving (Generic, Show)
208 instance (Serialise s, Serialise p) => Serialise (Archive s p)
211 type NodeListStory = NodeStory NgramsState' NgramsStatePatch'
213 type NgramsState' = Map TableNgrams.NgramsType NgramsTableMap
214 type NgramsStatePatch' = PatchMap TableNgrams.NgramsType NgramsTablePatch
215 instance Serialise NgramsStatePatch'
217 -- TODO Semigroup instance for unions
219 instance (Semigroup s, Semigroup p) => Semigroup (Archive s p) where
220 (<>) (Archive _v _s p) (Archive v' s' p') = Archive v' s' (p' <> p)
222 instance Monoid (Archive NgramsState' NgramsStatePatch') where
223 mempty = Archive 0 mempty []
225 instance (FromJSON s, FromJSON p) => FromJSON (Archive s p) where
226 parseJSON = genericParseJSON $ unPrefix "_a_"
228 instance (ToJSON s, ToJSON p) => ToJSON (Archive s p) where
229 toJSON = genericToJSON $ unPrefix "_a_"
230 toEncoding = genericToEncoding $ unPrefix "_a_"
232 ------------------------------------------------------------------------
234 initNodeStory :: Monoid s => NodeId -> NodeStory s p
235 initNodeStory ni = NodeStory $ Map.singleton ni initArchive
237 initArchive :: Monoid s => Archive s p
238 initArchive = Archive 0 mempty []
240 initNodeListStoryMock :: NodeListStory
241 initNodeListStoryMock = NodeStory $ Map.singleton nodeListId archive
244 archive = Archive 0 ngramsTableMap []
245 ngramsTableMap = Map.singleton TableNgrams.NgramsTerms
247 [ (n ^. ne_ngrams, ngramsElementToRepo n)
248 | n <- mockTable ^. _NgramsTable
251 ------------------------------------------------------------------------
252 -- | Lenses at the bottom of the file because Template Haskell would reorder order of execution in others cases
253 makeLenses ''NodeStoryEnv
254 makeLenses ''NodeStory