Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Guide/auto-refresh.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,15 @@ action MyAction = do -- <-- We don't enable auto refresh at the action start in
render MyView { expensiveModels, cheap }
```

### Smart Filtering

`autoRefresh` automatically tracks both the row IDs and the WHERE conditions of queries fetched during your action. This is used to skip unnecessary re-renders:

- **UPDATE / DELETE**: When a notification arrives for a row whose ID is not in the tracked set, the re-render is skipped.
- **INSERT**: When a new row is inserted, IHP evaluates your query's WHERE conditions against the inserted row. If the new row doesn't match your filters, the re-render is skipped. For example, if your action fetches `query @Task |> filterWhere (#projectId, myProjectId) |> fetch`, inserting a task with a different `projectId` will not trigger a re-render.

This happens transparently — no configuration needed. For tables accessed via raw SQL or `fetchCount` (where individual row IDs aren't available), auto refresh falls back to refreshing on every change. Conditions that can't be evaluated at notification time (e.g. `LIKE`, `LOWER()`, range operators) also fall back to refreshing.

### Custom SQL Queries with Auto Refresh

Auto Refresh automatically tracks all tables your action is using by hooking itself into the Query Builder and `fetch` functions.
Expand Down
320 changes: 270 additions & 50 deletions ihp/IHP/AutoRefresh.hs

Large diffs are not rendered by default.

63 changes: 61 additions & 2 deletions ihp/IHP/AutoRefresh/Types.hs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,64 @@ Copyright: (c) digitally induced GmbH, 2020
-}
module IHP.AutoRefresh.Types where

import IHP.Prelude
import Wai.Request.Params.Middleware (Respond)
import Control.Concurrent.MVar (MVar)
import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Types as AesonTypes
import qualified Data.UUID as UUID
import qualified IHP.PGListener as PGListener
import IHP.Prelude
import qualified Data.Map.Strict as Map
import Data.Dynamic (Dynamic)
import Network.Wai (Request)
import Wai.Request.Params.Middleware (Respond)

-- | A database operation that can trigger an auto refresh re-render.
data AutoRefreshOperation
= AutoRefreshInsert
| AutoRefreshUpdate
| AutoRefreshDelete
deriving (Eq, Show)

instance Aeson.FromJSON AutoRefreshOperation where
parseJSON = Aeson.withText "AutoRefreshOperation" \operation ->
case toLower operation of
"insert" -> pure AutoRefreshInsert
"update" -> pure AutoRefreshUpdate
"delete" -> pure AutoRefreshDelete
_ -> fail ("Unknown operation: " <> cs operation)

-- | Internal: raw payload sent by the PostgreSQL trigger.
--
-- For oversized payloads the trigger stores the full JSON in @large_pg_notifications@ and sends only a @payloadId@.
-- The auto refresh server resolves these @payloadId@s via a database lookup before building the change notification,
-- so smart filtering receives the full row json in @old@/@new@ (if payload resolution fails, auto refresh falls back
-- to forcing a refresh).
data AutoRefreshRowChangePayload = AutoRefreshRowChangePayload
{ payloadOperation :: !AutoRefreshOperation
, payloadOldRow :: !(Maybe Aeson.Value)
, payloadNewRow :: !(Maybe Aeson.Value)
, payloadLargePayloadId :: !(Maybe UUID.UUID)
} deriving (Eq, Show)

instance Aeson.FromJSON AutoRefreshRowChangePayload where
parseJSON = Aeson.withObject "AutoRefreshRowChangePayload" \object ->
AutoRefreshRowChangePayload
<$> object Aeson..: "op"
<*> object Aeson..:? "old"
<*> object Aeson..:? "new"
<*> do
payloadId <- object Aeson..:? "payloadId"
case payloadId of
Nothing -> pure Nothing
Just value -> Just <$> parseUUID value
where
parseUUID :: Text -> AesonTypes.Parser UUID.UUID
parseUUID value = case UUID.fromText value of
Just uuid -> pure uuid
Nothing -> fail "Invalid UUID for payloadId"

data AutoRefreshState = AutoRefreshEnabled { sessionId :: !UUID }

data AutoRefreshSession = AutoRefreshSession
{ id :: !UUID
-- | A callback to rerun an action within the given request and respond
Expand All @@ -24,6 +75,14 @@ data AutoRefreshSession = AutoRefreshSession
, lastResponse :: !LByteString
-- | Keep track of the last ping to this session to close it after too much time has passed without anything happening
, lastPing :: !UTCTime
-- | Tracked row IDs per table. 'Nothing' for a table means we can't filter (raw SQL / fetchCount).
-- 'Just ids' means only these IDs are relevant. Used by smart auto refresh to skip unrelated notifications.
, trackedIds :: !(Map.Map Text (Set Text))
-- | Tracked WHERE conditions per table. Each fetch appends its condition
-- (wrapped in 'Dynamic' to avoid a circular module dependency on 'Condition').
-- 'Nothing' means no condition (unfiltered query) — always refresh on INSERT.
-- Used by smart auto refresh to evaluate INSERT payloads against query filters.
, trackedConditions :: !(Map.Map Text [Maybe Dynamic])
}

data AutoRefreshServer = AutoRefreshServer
Expand Down
3 changes: 2 additions & 1 deletion ihp/IHP/ControllerPrelude.hs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import IHP.FetchPipelined
import IHP.FetchRelated
import Data.Aeson hiding (Success)
import Network.Wai.Parse (FileInfo(..))
import qualified Network.Wai
import IHP.RouterSupport hiding (get, post)
import IHP.Controller.Redirect
import Database.PostgreSQL.Simple.Types (Only (..))
Expand Down Expand Up @@ -91,5 +92,5 @@ import IHP.HSX.ToHtml ()
--
-- > setModal MyModalView { .. }
--
setModal :: (?context :: ControllerContext, ?request :: Request, View view) => view -> IO ()
setModal :: (?context :: ControllerContext, ?request :: Network.Wai.Request, View view) => view -> IO ()
setModal view = let ?view = view in Modal.setModal (ViewSupport.html view)
Loading