diff --git a/IHP/DataSync/ControllerImpl.hs b/IHP/DataSync/ControllerImpl.hs index 17fd66217..37577d9e2 100644 --- a/IHP/DataSync/ControllerImpl.hs +++ b/IHP/DataSync/ControllerImpl.hs @@ -187,6 +187,49 @@ buildMessageHandler ensureRLSEnabled installTableChangeTriggers sendJSON handleC MVar.takeMVar close + handleMessage CreateCountSubscription { query, requestId } = do + ensureBelowSubscriptionsLimit + + tableNameRLS <- ensureRLSEnabled query.table + + subscriptionId <- UUID.nextRandom + + -- Allocate the close handle as early as possible + -- to make DeleteDataSubscription calls succeed even when the CountSubscription is + -- not fully set up yet + close <- MVar.newEmptyMVar + atomicModifyIORef'' ?state (\state -> state |> modify #subscriptions (HashMap.insert subscriptionId close)) + + let (theQuery, theParams) = compileQueryWithRenamer (renamer query.table) query + + let countQuery = "SELECT COUNT(*) FROM (" <> theQuery <> ") AS _inner" + + let + unpackResult :: [(Only Int)] -> Int + unpackResult [(Only value)] = value + unpackResult otherwise = error "DataSync.unpackResult: Expected INT, but got something else" + + count <- unpackResult <$> sqlQueryWithRLS countQuery theParams + countRef <- newIORef count + + installTableChangeTriggers tableNameRLS + + let + callback :: ChangeNotifications.ChangeNotification -> IO () + callback _ = do + newCount <- unpackResult <$> sqlQueryWithRLS countQuery theParams + lastCount <- readIORef countRef + + when (newCount /= count) (sendJSON DidChangeCount { subscriptionId, count = newCount }) + + let subscribe = PGListener.subscribeJSON (ChangeNotifications.channelName tableNameRLS) callback pgListener + let unsubscribe subscription = PGListener.unsubscribe subscription pgListener + + Exception.bracket subscribe unsubscribe \channelSubscription -> do + sendJSON DidCreateCountSubscription { subscriptionId, requestId, count } + + MVar.takeMVar close + handleMessage DeleteDataSubscription { requestId, subscriptionId } = do DataSyncReady { subscriptions } <- getState case HashMap.lookup subscriptionId subscriptions of diff --git a/IHP/DataSync/Types.hs b/IHP/DataSync/Types.hs index d93be685f..ed4240d7e 100644 --- a/IHP/DataSync/Types.hs +++ b/IHP/DataSync/Types.hs @@ -10,6 +10,7 @@ import Control.Concurrent.MVar as MVar data DataSyncMessage = DataSyncQuery { query :: !DynamicSQLQuery, requestId :: !Int, transactionId :: !(Maybe UUID) } | CreateDataSubscription { query :: !DynamicSQLQuery, requestId :: !Int } + | CreateCountSubscription { query :: !DynamicSQLQuery, requestId :: !Int } | DeleteDataSubscription { subscriptionId :: !UUID, requestId :: !Int } | CreateRecordMessage { table :: !Text, record :: !(HashMap Text Value), requestId :: !Int, transactionId :: !(Maybe UUID) } | CreateRecordsMessage { table :: !Text, records :: ![HashMap Text Value], requestId :: !Int, transactionId :: !(Maybe UUID) } @@ -31,10 +32,12 @@ data DataSyncResponse | DataSyncError { requestId :: !Int, errorMessage :: !Text } | FailedToDecodeMessageError { errorMessage :: !Text } | DidCreateDataSubscription { requestId :: !Int, subscriptionId :: !UUID, result :: ![[Field]] } + | DidCreateCountSubscription { requestId :: !Int, subscriptionId :: !UUID, count :: !Int } | DidDeleteDataSubscription { requestId :: !Int, subscriptionId :: !UUID } | DidInsert { subscriptionId :: !UUID, record :: ![Field] } | DidUpdate { subscriptionId :: !UUID, id :: UUID, changeSet :: !Value } | DidDelete { subscriptionId :: !UUID, id :: !UUID } + | DidChangeCount { subscriptionId :: !UUID, count :: !Int } | DidCreateRecord { requestId :: !Int, record :: ![Field] } -- ^ Response to 'CreateRecordMessage' | DidCreateRecords { requestId :: !Int, records :: ![[Field]] } -- ^ Response to 'CreateRecordsMessage' | DidUpdateRecord { requestId :: !Int, record :: ![Field] } -- ^ Response to 'UpdateRecordMessage' diff --git a/ihp-datasync-typescript/IHP/DataSync/TypeScript/Compiler.hs b/ihp-datasync-typescript/IHP/DataSync/TypeScript/Compiler.hs index c59539967..1656e37e2 100644 --- a/ihp-datasync-typescript/IHP/DataSync/TypeScript/Compiler.hs +++ b/ihp-datasync-typescript/IHP/DataSync/TypeScript/Compiler.hs @@ -259,6 +259,8 @@ declare module 'ihp-datasync/react' { */ function useQuery