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 Debug.Trace (traceShow)
19 import Codec.Serialise (serialise, deserialise)
20 import Codec.Serialise.Class
21 import Control.Concurrent (MVar(), withMVar, newMVar, modifyMVar_)
22 import Control.Debounce (mkDebounce, defaultDebounceSettings, debounceFreq, debounceAction)
23 import Control.Lens (makeLenses, Getter, (^.))
24 import Control.Monad.Except
25 import Control.Monad.Reader
26 import Data.Aeson hiding ((.=), decode)
27 import Data.Map.Strict (Map)
28 import Data.Maybe (fromMaybe)
31 import GHC.Generics (Generic)
32 import Gargantext.API.Ngrams.Types
33 import Gargantext.Core.Types (NodeId)
34 import Gargantext.Core.Utils.Prefix (unPrefix)
35 import Gargantext.Database.Prelude (CmdM', HasConnectionPool, HasConfig)
36 import Gargantext.Database.Query.Table.Node.Error (HasNodeError())
37 import Gargantext.Prelude
38 import System.Directory (renameFile, createDirectoryIfMissing, doesFileExist)
39 import System.IO (FilePath, hClose)
40 import System.IO.Temp (withTempFile)
41 import qualified Data.ByteString.Lazy as DBL
42 import qualified Data.List as List
43 import qualified Data.Map.Strict as Map
44 import qualified Data.Map.Strict.Patch.Internal as Patch
45 import qualified Gargantext.Database.Query.Table.Ngrams as TableNgrams
47 ------------------------------------------------------------------------
48 data NodeStoryEnv = NodeStoryEnv
49 { _nse_var :: !(MVar NodeListStory)
50 , _nse_saver :: !(IO ())
51 , _nse_getter :: NodeId -> IO (MVar NodeListStory)
52 --, _nse_cleaner :: !(IO ()) -- every 12 hours: cleans the repos of unused NodeStories
53 -- , _nse_lock :: !FileLock -- TODO (it depends on the option: if with database or file only)
57 type HasNodeStory env err m = ( CmdM' env err m
62 , HasConnectionPool env
66 class (HasNodeStoryVar env, HasNodeStorySaver env)
67 => HasNodeStoryEnv env where
68 hasNodeStory :: Getter env NodeStoryEnv
70 class HasNodeStoryVar env where
71 hasNodeStoryVar :: Getter env (NodeId -> IO (MVar NodeListStory))
73 class HasNodeStorySaver env where
74 hasNodeStorySaver :: Getter env (IO ())
76 ------------------------------------------------------------------------
77 readNodeStoryEnv :: NodeStoryDir -> IO NodeStoryEnv
78 readNodeStoryEnv nsd = do
79 mvar <- nodeStoryVar nsd Nothing 0
80 saver <- mkNodeStorySaver nsd mvar
81 pure $ NodeStoryEnv mvar saver (nodeStoryVar nsd (Just mvar))
83 ------------------------------------------------------------------------
84 mkNodeStorySaver :: NodeStoryDir -> MVar NodeListStory -> IO (IO ())
85 mkNodeStorySaver nsd mvns = mkDebounce settings
87 settings = defaultDebounceSettings
88 { debounceAction = withMVar mvns (writeNodeStories nsd)
89 , debounceFreq = 1 * minute
90 -- , debounceEdge = trailingEdge -- Trigger on the trailing edge
93 second = 10^(6 :: Int)
95 nodeStoryVar :: NodeStoryDir
96 -> Maybe (MVar NodeListStory)
98 -> IO (MVar NodeListStory)
99 nodeStoryVar nsd Nothing ni = nodeStoryInc nsd Nothing ni >>= newMVar
100 nodeStoryVar nsd (Just mv) ni = do
101 _ <- modifyMVar_ mv $ \mv' -> (nodeStoryInc nsd (Just mv') ni)
105 nodeStoryInc :: NodeStoryDir -> Maybe NodeListStory -> NodeId -> IO NodeListStory
106 nodeStoryInc nsd (Just ns@(NodeStory nls)) ni = do
107 case Map.lookup ni nls of
109 (NodeStory nls') <- nodeStoryRead nsd ni
110 pure $ NodeStory $ Map.union nls nls'
112 nodeStoryInc nsd Nothing ni = nodeStoryRead nsd ni
116 nodeStoryRead :: NodeStoryDir -> NodeId -> IO NodeListStory
117 nodeStoryRead nsd ni = do
118 _repoDir <- createDirectoryIfMissing True nsd
119 let nsp = nodeStoryPath nsd ni
120 exists <- doesFileExist nsp
122 then deserialise <$> DBL.readFile nsp
123 else pure (initNodeStory ni)
125 nodeStoryRead_test :: NodeStoryDir -> NodeId -> IO (Maybe [ TableNgrams.NgramsType ])
126 nodeStoryRead_test nsd ni = nodeStoryRead nsd ni >>= \n -> pure
132 ------------------------------------------------------------------------
133 type NodeStoryDir = FilePath
135 writeNodeStories :: NodeStoryDir -> NodeListStory -> IO ()
136 writeNodeStories fp nls = do
137 done <- mapM (writeNodeStory fp) $ splitByNode nls
138 printDebug "[writeNodeStories]" done
141 writeNodeStory :: NodeStoryDir -> (NodeId, NodeListStory) -> IO ()
142 writeNodeStory rdfp (n, ns) = saverAction' rdfp n ns
144 splitByNode :: NodeListStory -> [(NodeId, NodeListStory)]
145 splitByNode (NodeStory m) =
146 List.map (\(n,a) -> (n, NodeStory $ Map.singleton n a)) $ Map.toList m
149 saverAction' :: Serialise a => NodeStoryDir -> NodeId -> a -> IO ()
150 saverAction' repoDir nId a = do
151 withTempFile repoDir ((cs $ show nId) <> "-tmp-repo.cbor") $ \fp h -> do
152 printDebug "[repoSaverAction]" fp
153 DBL.hPut h $ serialise a
155 renameFile fp (nodeStoryPath repoDir nId)
157 nodeStoryPath :: NodeStoryDir -> NodeId -> FilePath
158 nodeStoryPath repoDir nId = repoDir <> "/" <> filename
160 filename = "repo" <> "-" <> (cs $ show nId) <> ".cbor"
163 ------------------------------------------------------------------------
164 -- TODO : repo Migration TODO TESTS
165 repoMigration :: NodeStoryDir -> NgramsRepo -> IO ()
166 repoMigration fp r = writeNodeStories fp (repoToNodeListStory r)
168 repoToNodeListStory :: NgramsRepo -> NodeListStory
169 repoToNodeListStory (Repo _v s h) = NodeStory $ Map.fromList ns
171 s' = ngramsState_migration s
172 h' = ngramsStatePatch_migration h
173 ns = List.map (\(n,ns')
174 -> (n, let hs = fromMaybe [] (Map.lookup n h') in
175 Archive (List.length hs) ns' hs
179 ngramsState_migration :: NgramsState
180 -> Map NodeId NgramsState'
181 ngramsState_migration ns =
182 Map.fromListWith (Map.union) $
185 -> map (\(nid, table)
186 -> (nid, Map.singleton nt table)
187 ) $ Map.toList nTable
191 ngramsStatePatch_migration :: [NgramsStatePatch]
192 -> Map NodeId [NgramsStatePatch']
193 ngramsStatePatch_migration np' = Map.fromListWith (<>)
197 toPatch :: NgramsStatePatch -> [(NodeId, [NgramsStatePatch'])]
201 -> map (\(nid, table)
202 -> (nid, [fst $ Patch.singleton nt table])
203 ) $ Patch.toList nTable
206 ------------------------------------------------------------------------
208 {- | Node Story for each NodeType where the Key of the Map is NodeId
209 TODO : generalize for any NodeType, let's start with NodeList which
210 is implemented already
212 data NodeStory s p = NodeStory { _unNodeStory :: Map NodeId (Archive s p) }
213 deriving (Generic, Show)
215 instance (FromJSON s, FromJSON p) => FromJSON (NodeStory s p)
216 instance (ToJSON s, ToJSON p) => ToJSON (NodeStory s p)
217 instance (Serialise s, Serialise p) => Serialise (NodeStory s p)
219 data Archive s p = Archive
220 { _a_version :: !Version
223 -- first patch in the list is the most recent
225 deriving (Generic, Show)
227 instance (Serialise s, Serialise p) => Serialise (Archive s p)
230 type NodeListStory = NodeStory NgramsState' NgramsStatePatch'
232 type NgramsState' = Map TableNgrams.NgramsType NgramsTableMap
233 type NgramsStatePatch' = PatchMap TableNgrams.NgramsType NgramsTablePatch
234 instance Serialise NgramsStatePatch'
236 -- TODO Semigroup instance for unions
238 instance (Semigroup s, Semigroup p) => Semigroup (Archive s p) where
239 (<>) (Archive _v _s p) (Archive v' s' p') = Archive v' s' (p' <> p)
241 instance Monoid (Archive NgramsState' NgramsStatePatch') where
242 mempty = Archive 0 mempty []
244 instance (FromJSON s, FromJSON p) => FromJSON (Archive s p) where
245 parseJSON = genericParseJSON $ unPrefix "_a_"
247 instance (ToJSON s, ToJSON p) => ToJSON (Archive s p) where
248 toJSON = genericToJSON $ unPrefix "_a_"
249 toEncoding = genericToEncoding $ unPrefix "_a_"
251 ------------------------------------------------------------------------
252 initNodeStory :: Monoid s => NodeId -> NodeStory s p
253 initNodeStory ni = NodeStory $ Map.singleton ni initArchive
255 initArchive :: Monoid s => Archive s p
256 initArchive = Archive 0 mempty []
258 initNodeListStoryMock :: NodeListStory
259 initNodeListStoryMock = NodeStory $ Map.singleton nodeListId archive
262 archive = Archive 0 ngramsTableMap []
263 ngramsTableMap = Map.singleton TableNgrams.NgramsTerms
265 [ (n ^. ne_ngrams, ngramsElementToRepo n)
266 | n <- mockTable ^. _NgramsTable
269 ------------------------------------------------------------------------
272 ------------------------------------------------------------------------
273 -- | Lenses at the bottom of the file because Template Haskell would reorder order of execution in others cases
274 makeLenses ''NodeStoryEnv
275 makeLenses ''NodeStory