Optimize getUnreadByConversationAndMarkRead

This commit is contained in:
trevor-signal
2025-11-20 12:49:49 -05:00
committed by GitHub
parent bd69748a3d
commit 42af9b5c3d

View File

@@ -3334,16 +3334,32 @@ export function _storyIdPredicate(
storyId: string | undefined, storyId: string | undefined,
includeStoryReplies: boolean includeStoryReplies: boolean
): QueryFragment { ): QueryFragment {
return _storyIdPredicateAndInfo(storyId, includeStoryReplies).predicate;
}
function _storyIdPredicateAndInfo(
storyId: string | undefined,
includeStoryReplies: boolean
): {
predicate: QueryFragment;
isFilteringOnStoryId: boolean;
} {
// This is unintuitive, but 'including story replies' means that we need replies to // This is unintuitive, but 'including story replies' means that we need replies to
// lots of different stories. So, we remove the storyId check with a clause that will // lots of different stories. So, we remove the storyId check with a clause that will
// always be true. We don't just return TRUE because we want to use our passed-in // always be true. We don't just return TRUE because we want to use our passed-in
// $storyId parameter. // $storyId parameter.
if (includeStoryReplies && storyId === undefined) { if (includeStoryReplies && storyId === undefined) {
return sqlFragment`NULL IS NULL`; return {
predicate: sqlFragment`NULL IS NULL`,
isFilteringOnStoryId: false,
};
} }
// In contrast to: replies to a specific story // In contrast to: replies to a specific story
return sqlFragment`storyId IS ${storyId ?? null}`; return {
predicate: sqlFragment`storyId IS ${storyId ?? null}`,
isFilteringOnStoryId: true,
};
} }
function getUnreadByConversationAndMarkRead( function getUnreadByConversationAndMarkRead(
@@ -3367,6 +3383,9 @@ function getUnreadByConversationAndMarkRead(
return db.transaction(() => { return db.transaction(() => {
const expirationStartTimestamp = Math.min(now, readAt ?? Infinity); const expirationStartTimestamp = Math.min(now, readAt ?? Infinity);
const { predicate: storyReplyFilter, isFilteringOnStoryId } =
_storyIdPredicateAndInfo(storyId, includeStoryReplies);
const updateExpirationFragment = sqlFragment` const updateExpirationFragment = sqlFragment`
UPDATE messages UPDATE messages
INDEXED BY messages_conversationId_expirationStartTimestamp INDEXED BY messages_conversationId_expirationStartTimestamp
@@ -3374,7 +3393,7 @@ function getUnreadByConversationAndMarkRead(
expirationStartTimestamp = ${expirationStartTimestamp} expirationStartTimestamp = ${expirationStartTimestamp}
WHERE WHERE
conversationId = ${conversationId} AND conversationId = ${conversationId} AND
(${_storyIdPredicate(storyId, includeStoryReplies)}) AND ${storyReplyFilter} AND
type IN ('incoming', 'poll-terminate') AND type IN ('incoming', 'poll-terminate') AND
hasExpireTimer IS 1 AND hasExpireTimer IS 1 AND
received_at <= ${readMessageReceivedAt} received_at <= ${readMessageReceivedAt}
@@ -3403,56 +3422,68 @@ function getUnreadByConversationAndMarkRead(
updateLateExpirationStartParams updateLateExpirationStartParams
); );
const indexToUse = isFilteringOnStoryId
? sqlFragment`messages_unseen_with_story`
: sqlFragment`messages_unseen_no_story`;
const [selectQuery, selectParams] = sql` const [selectQuery, selectParams] = sql`
SELECT SELECT
${sqlJoin(MESSAGE_COLUMNS_FRAGMENTS)} id, readStatus, expirationStartTimestamp, sent_at, source, sourceServiceId, type
FROM messages FROM messages
INDEXED BY ${indexToUse}
WHERE WHERE
conversationId = ${conversationId} AND conversationId = ${conversationId} AND
seenStatus = ${SeenStatus.Unseen} AND seenStatus = ${SeenStatus.Unseen} AND
isStory = 0 AND isStory = 0 AND
(${_storyIdPredicate(storyId, includeStoryReplies)}) AND ${storyReplyFilter} AND
received_at <= ${readMessageReceivedAt} received_at <= ${readMessageReceivedAt}
ORDER BY received_at DESC, sent_at DESC; ORDER BY received_at DESC, sent_at DESC;
`; `;
const rows = db const rows = db
.prepare(selectQuery) .prepare(selectQuery)
.all<MessageTypeUnhydrated>(selectParams); .all<
Pick<
const statusJsonPatch = JSON.stringify({ MessageTypeUnhydrated,
readStatus: ReadStatus.Read, | 'id'
seenStatus: SeenStatus.Seen, | 'readStatus'
}); | 'expirationStartTimestamp'
| 'sent_at'
| 'source'
| 'sourceServiceId'
| 'type'
>
>(selectParams);
const [updateStatusQuery, updateStatusParams] = sql` const [updateStatusQuery, updateStatusParams] = sql`
UPDATE messages UPDATE messages
INDEXED BY ${indexToUse}
SET SET
readStatus = ${ReadStatus.Read}, readStatus = ${ReadStatus.Read},
seenStatus = ${SeenStatus.Seen}, seenStatus = ${SeenStatus.Seen}
json = json_patch(json, ${statusJsonPatch})
WHERE WHERE
conversationId = ${conversationId} AND conversationId = ${conversationId} AND
seenStatus = ${SeenStatus.Unseen} AND seenStatus = ${SeenStatus.Unseen} AND
isStory = 0 AND isStory = 0 AND
(${_storyIdPredicate(storyId, includeStoryReplies)}) AND ${storyReplyFilter} AND
received_at <= ${readMessageReceivedAt}; received_at <= ${readMessageReceivedAt};
`; `;
db.prepare(updateStatusQuery).run(updateStatusParams); db.prepare(updateStatusQuery).run(updateStatusParams);
return hydrateMessages(db, rows).map(msg => { return rows.map(msg => {
return { return {
originalReadStatus: msg.readStatus, originalReadStatus:
msg.readStatus == null ? undefined : (msg.readStatus as ReadStatus),
readStatus: ReadStatus.Read, readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen, seenStatus: SeenStatus.Seen,
...pick(msg, [ id: msg.id,
'expirationStartTimestamp', expirationStartTimestamp: dropNull(msg.expirationStartTimestamp),
'id', sent_at: msg.sent_at || 0,
'sent_at', source: dropNull(msg.source),
'source', sourceServiceId: dropNull(msg.sourceServiceId) as
'sourceServiceId', | ServiceIdString
'type', | undefined,
]), type: msg.type as MessageType['type'],
}; };
}); });
})(); })();